From a573d5a7ee890f4334c417f216345096b9fb182f Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Thu, 26 Dec 2024 16:38:33 +0000 Subject: [PATCH 01/16] [this should go away when we eventually rebase] --- src/Makefile.test.include | 3 + src/bench/coin_selection.cpp | 8 +- src/wallet/coinselection.cpp | 52 ++- src/wallet/coinselection.h | 52 ++- src/wallet/test/coinselector_tests.cpp | 144 +++--- src/wallet/test/spend_tests.cpp | 61 +++ src/wallet/test/util.cpp | 38 ++ src/wallet/test/util.h | 22 + src/wallet/test/wallet_tests.cpp | 17 +- src/wallet/wallet.cpp | 621 ++++++++++++------------- src/wallet/wallet.h | 56 +-- 11 files changed, 560 insertions(+), 514 deletions(-) create mode 100644 src/wallet/test/spend_tests.cpp create mode 100644 src/wallet/test/util.cpp create mode 100644 src/wallet/test/util.h diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 00e0429688fd7..2771782a68617 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -194,6 +194,7 @@ BITCOIN_TESTS += \ wallet/test/bip39_tests.cpp \ wallet/test/coinjoin_tests.cpp \ wallet/test/psbt_wallet_tests.cpp \ + wallet/test/spend_tests.cpp \ wallet/test/wallet_tests.cpp \ wallet/test/walletdb_tests.cpp \ wallet/test/wallet_crypto_tests.cpp \ @@ -213,6 +214,8 @@ endif BITCOIN_TEST_SUITE += \ + wallet/test/util.cpp \ + wallet/test/util.h \ wallet/test/wallet_test_fixture.cpp \ wallet/test/wallet_test_fixture.h \ wallet/test/init_test_fixture.cpp \ diff --git a/src/bench/coin_selection.cpp b/src/bench/coin_selection.cpp index e53755c046c19..da29c8bddbb92 100644 --- a/src/bench/coin_selection.cpp +++ b/src/bench/coin_selection.cpp @@ -48,15 +48,14 @@ static void CoinSelection(benchmark::Bench& bench) coins.emplace_back(wtx.get(), 0 /* iIn */, 6 * 24 /* nDepthIn */, true /* spendable */, true /* solvable */, true /* safe */); } const CoinEligibilityFilter filter_standard(1, 6, 0); - const CoinSelectionParams coin_selection_params(/* use_bnb= */ true, /* change_output_size= */ 34, + const CoinSelectionParams coin_selection_params(/* change_output_size= */ 34, /* change_spend_size= */ 148, /* effective_feerate= */ CFeeRate(0), /* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0), /* tx_no_inputs_size= */ 0, /* avoid_partial= */ false); bench.run([&] { std::set setCoinsRet; CAmount nValueRet; - bool bnb_used; - bool success = wallet.SelectCoinsMinConf(1003 * COIN, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used); + bool success = wallet.SelectCoinsMinConf(1003 * COIN, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params); assert(success); assert(nValueRet == 1003 * COIN); assert(setCoinsRet.size() == 2); @@ -94,12 +93,11 @@ static void BnBExhaustion(benchmark::Bench& bench) std::vector utxo_pool; CoinSet selection; CAmount value_ret = 0; - CAmount not_input_fees = 0; bench.run([&] { // Benchmark CAmount target = make_hard_case(17, utxo_pool); - SelectCoinsBnB(utxo_pool, target, 0, selection, value_ret, not_input_fees); // Should exhaust + SelectCoinsBnB(utxo_pool, target, 0, selection, value_ret); // Should exhaust // Cleanup utxo_pool.clear(); diff --git a/src/wallet/coinselection.cpp b/src/wallet/coinselection.cpp index 6bd9044da8fa9..a657b282c3b07 100644 --- a/src/wallet/coinselection.cpp +++ b/src/wallet/coinselection.cpp @@ -16,7 +16,7 @@ struct { bool operator()(const OutputGroup& a, const OutputGroup& b) const { - return a.effective_value > b.effective_value; + return a.GetSelectionAmount() > b.GetSelectionAmount(); } } descending; @@ -51,37 +51,34 @@ struct { * @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 CAmount& target_value This is the value that we want to select. It is the lower + * @param const CAmount& selection_target This is the value that we want to select. It is the lower * bound of the range. * @param const CAmount& 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. + * This plus selection_target 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 CAmount& value_ret -> This is an output parameter for the total value of the CInputCoins * that were selected. - * @param CAmount 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, const CAmount& target_value, const CAmount& cost_of_change, std::set& out_set, CAmount& value_ret, CAmount not_input_fees) +bool SelectCoinsBnB(std::vector& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change, std::set& out_set, CAmount& value_ret) { out_set.clear(); CAmount curr_value = 0; std::vector curr_selection; // select the utxo at this index curr_selection.reserve(utxo_pool.size()); - CAmount actual_target = not_input_fees + target_value; // Calculate curr_available_value CAmount curr_available_value = 0; 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 > 0); - curr_available_value += utxo.effective_value; + assert(utxo.GetSelectionAmount() > 0); + curr_available_value += utxo.GetSelectionAmount(); } - if (curr_available_value < actual_target) { + if (curr_available_value < selection_target) { return false; } @@ -96,12 +93,12 @@ bool SelectCoinsBnB(std::vector& utxo_pool, const CAmount& target_v 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 + if (curr_value + curr_available_value < selection_target || // Cannot possibly reach target with the amount remaining in the curr_available_value. + curr_value > selection_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) > 0)) { // Don't select things which we know will be more wasteful if the waste is increasing backtrack = true; - } else if (curr_value >= actual_target) { // Selected value is within range - curr_waste += (curr_value - actual_target); // This is the excess value which is added to the waste for the below comparison + } else if (curr_value >= selection_target) { // Selected value is within range + curr_waste += (curr_value - selection_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 @@ -114,7 +111,7 @@ bool SelectCoinsBnB(std::vector& utxo_pool, const CAmount& target_v break; } } - curr_waste -= (curr_value - actual_target); // Remove the excess value as we will be selecting different coins now + curr_waste -= (curr_value - selection_target); // Remove the excess value as we will be selecting different coins now backtrack = true; } @@ -123,7 +120,7 @@ bool SelectCoinsBnB(std::vector& utxo_pool, const CAmount& target_v // 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; + curr_available_value += utxo_pool.at(curr_selection.size()).GetSelectionAmount(); } if (curr_selection.empty()) { // We have walked back to the first utxo and no branch is untraversed. All solutions searched @@ -133,24 +130,24 @@ bool SelectCoinsBnB(std::vector& utxo_pool, const CAmount& target_v // Output was included on previous iterations, try excluding now. curr_selection.back() = false; OutputGroup& utxo = utxo_pool.at(curr_selection.size() - 1); - curr_value -= utxo.effective_value; + curr_value -= utxo.GetSelectionAmount(); curr_waste -= utxo.fee - utxo.long_term_fee; } else { // Moving forwards, continuing down this branch OutputGroup& utxo = utxo_pool.at(curr_selection.size()); // Remove this utxo from the curr_available_value utxo amount - curr_available_value -= utxo.effective_value; + curr_available_value -= utxo.GetSelectionAmount(); // 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.GetSelectionAmount() == utxo_pool.at(curr_selection.size() - 1).GetSelectionAmount() && 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_value += utxo.GetSelectionAmount(); curr_waste += utxo.fee - utxo.long_term_fee; } } @@ -283,14 +280,14 @@ bool KnapsackSolver(const CAmount& nTargetValue, std::vector& group if (tryDenom == 0 && CoinJoin::IsDenominatedAmount(group.m_value)) { continue; // we don't want denom values on first run } - if (group.m_value == nTargetValue) { + if (group.GetSelectionAmount() == nTargetValue) { util::insert(setCoinsRet, group.m_outputs); nValueRet += group.m_value; return true; - } else if (group.m_value < nTargetValue + nMinChange) { + } else if (group.GetSelectionAmount() < nTargetValue + nMinChange) { applicable_groups.push_back(group); - nTotalLower += group.m_value; - } else if (!lowest_larger || group.m_value < lowest_larger->m_value) { + nTotalLower += group.GetSelectionAmount(); + } else if (!lowest_larger || group.GetSelectionAmount() < lowest_larger->GetSelectionAmount()) { lowest_larger = group; } } @@ -336,7 +333,7 @@ bool KnapsackSolver(const CAmount& nTargetValue, std::vector& group // 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 (lowest_larger && - ((nBest != nTargetValue && nBest < nTargetValue + nMinChange) || lowest_larger->m_value <= nBest)) { + ((nBest != nTargetValue && nBest < nTargetValue + nMinChange) || lowest_larger->GetSelectionAmount() <= nBest)) { util::insert(setCoinsRet, lowest_larger->m_outputs); nValueRet += lowest_larger->m_value; } else { @@ -400,3 +397,8 @@ bool OutputGroup::EligibleForSpending(const CoinEligibilityFilter& eligibility_f && m_ancestors <= eligibility_filter.max_ancestors && m_descendants <= eligibility_filter.max_descendants; } + +CAmount OutputGroup::GetSelectionAmount() const +{ + return m_subtract_fee_outputs ? m_value : effective_value; +} diff --git a/src/wallet/coinselection.h b/src/wallet/coinselection.h index 487636ffa413c..cbe80d87c682b 100644 --- a/src/wallet/coinselection.h +++ b/src/wallet/coinselection.h @@ -57,6 +57,45 @@ class CInputCoin { } }; +/** Parameters for one iteration of Coin Selection. */ +struct CoinSelectionParams +{ + /** Size of a change output in bytes, determined by the output type. */ + size_t change_output_size = 0; + /** Size of the input to spend a change output in virtual bytes. */ + size_t change_spend_size = 0; + /** Cost of creating the change output. */ + CAmount m_change_fee{0}; + /** Cost of creating the change output + cost of spending the change output in the future. */ + CAmount m_cost_of_change{0}; + /** The fee to spend these UTXOs at the long term feerate. */ + CFeeRate m_effective_feerate; + /** The feerate estimate used to estimate an upper bound on what should be sufficient to spend + * the change output sometime in the future. */ + CFeeRate m_long_term_feerate; + /** If the cost to spend a change output at the discard feerate exceeds its value, drop it to fees. */ + CFeeRate m_discard_feerate; + size_t tx_noinputs_size = 0; + /** Indicate that we are subtracting the fee from outputs */ + bool m_subtract_fee_outputs = false; + /** When true, always spend all (up to OUTPUT_GROUP_MAX_ENTRIES) or none of the outputs + * associated with the same address. This helps reduce privacy leaks resulting from address + * reuse. Dust outputs are not eligible to be added to output groups and thus not considered. */ + bool m_avoid_partial_spends = false; + + CoinSelectionParams(size_t change_output_size, size_t change_spend_size, CFeeRate effective_feerate, + CFeeRate long_term_feerate, CFeeRate discard_feerate, size_t tx_noinputs_size, bool avoid_partial) : + change_output_size(change_output_size), + change_spend_size(change_spend_size), + m_effective_feerate(effective_feerate), + m_long_term_feerate(long_term_feerate), + m_discard_feerate(discard_feerate), + tx_noinputs_size(tx_noinputs_size), + m_avoid_partial_spends(avoid_partial) + {} + CoinSelectionParams() {} +}; + /** Parameters for filtering which OutputGroups we may use in coin selection. * We start by being very selective and requiring multiple confirmations and * then get more permissive if we cannot fund the transaction. */ @@ -109,18 +148,23 @@ struct OutputGroup * a lower feerate). Calculated using long term fee estimate. This is used to decide whether * it could be economical to create a change output. */ CFeeRate m_long_term_feerate{0}; + /** Indicate that we are subtracting the fee from outputs. + * When true, the value that is used for coin selection is the UTXO's real value rather than effective value */ + bool m_subtract_fee_outputs{false}; OutputGroup() {} - OutputGroup(const CFeeRate& effective_feerate, const CFeeRate& long_term_feerate) : - m_effective_feerate(effective_feerate), - m_long_term_feerate(long_term_feerate) + OutputGroup(const CoinSelectionParams& params) : + m_effective_feerate(params.m_effective_feerate), + m_long_term_feerate(params.m_long_term_feerate), + m_subtract_fee_outputs(params.m_subtract_fee_outputs) {} void Insert(const CInputCoin& output, int depth, bool from_me, size_t ancestors, size_t descendants, bool positive_only); bool EligibleForSpending(const CoinEligibilityFilter& eligibility_filter, bool isISLocked) const; + CAmount GetSelectionAmount() const; }; -bool SelectCoinsBnB(std::vector& utxo_pool, const CAmount& target_value, const CAmount& cost_of_change, std::set& out_set, CAmount& value_ret, CAmount not_input_fees); +bool SelectCoinsBnB(std::vector& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change, std::set& out_set, CAmount& value_ret); // Original coin selection algorithm as a fallback bool KnapsackSolver(const CAmount& nTargetValue, std::vector& groups, std::set& setCoinsRet, CAmount& nValueRet, bool fFulyMixedOnly, CAmount maxTxFee); diff --git a/src/wallet/test/coinselector_tests.cpp b/src/wallet/test/coinselector_tests.cpp index e7731a9c4db3c..1cf9cbde0b8b2 100644 --- a/src/wallet/test/coinselector_tests.cpp +++ b/src/wallet/test/coinselector_tests.cpp @@ -34,7 +34,7 @@ static const CoinEligibilityFilter filter_standard(1, 6, 0); static const CoinEligibilityFilter filter_confirmed(1, 1, 0); static const CoinEligibilityFilter filter_standard_extra(6, 6, 0); -CoinSelectionParams coin_selection_params(/* use_bnb= */ false, /* change_output_size= */ 0, +CoinSelectionParams coin_selection_params(/* change_output_size= */ 0, /* change_spend_size= */ 0, /* effective_feerate= */ CFeeRate(0), /* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0), /* tx_no_inputs_size= */ 0, /* avoid_partial= */ false); @@ -132,14 +132,13 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) CoinSet selection; CoinSet actual_selection; CAmount value_ret = 0; - CAmount not_input_fees = 0; ///////////////////////// // Known Outcome tests // ///////////////////////// // Empty utxo pool - BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, 0.5 * CENT, selection, value_ret, not_input_fees)); + BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, 0.5 * CENT, selection, value_ret)); selection.clear(); // Add utxos @@ -150,7 +149,7 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) // Select 1 Cent add_coin(1 * CENT, 1, actual_selection); - BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, 0.5 * CENT, selection, value_ret, not_input_fees)); + BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, 0.5 * CENT, selection, value_ret)); BOOST_CHECK(equal_sets(selection, actual_selection)); BOOST_CHECK_EQUAL(value_ret, 1 * CENT); actual_selection.clear(); @@ -158,7 +157,7 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) // Select 2 Cent add_coin(2 * CENT, 2, actual_selection); - BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 2 * CENT, 0.5 * CENT, selection, value_ret, not_input_fees)); + BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 2 * CENT, 0.5 * CENT, selection, value_ret)); BOOST_CHECK(equal_sets(selection, actual_selection)); BOOST_CHECK_EQUAL(value_ret, 2 * CENT); actual_selection.clear(); @@ -167,27 +166,27 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) // Select 5 Cent add_coin(4 * CENT, 4, actual_selection); add_coin(1 * CENT, 1, actual_selection); - BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 5 * CENT, 0.5 * CENT, selection, value_ret, not_input_fees)); + BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 5 * CENT, 0.5 * CENT, selection, value_ret)); BOOST_CHECK(equal_sets(selection, actual_selection)); BOOST_CHECK_EQUAL(value_ret, 5 * CENT); actual_selection.clear(); selection.clear(); // Select 11 Cent, not possible - BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 11 * CENT, 0.5 * CENT, selection, value_ret, not_input_fees)); + BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 11 * CENT, 0.5 * CENT, selection, value_ret)); actual_selection.clear(); selection.clear(); // Cost of change is greater than the difference between target value and utxo sum add_coin(1 * CENT, 1, actual_selection); - BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 0.9 * CENT, 0.5 * CENT, selection, value_ret, not_input_fees)); + BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 0.9 * CENT, 0.5 * CENT, selection, value_ret)); BOOST_CHECK_EQUAL(value_ret, 1 * CENT); BOOST_CHECK(equal_sets(selection, actual_selection)); actual_selection.clear(); selection.clear(); // Cost of change is less than the difference between target value and utxo sum - BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 0.9 * CENT, 0, selection, value_ret, not_input_fees)); + BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 0.9 * CENT, 0, selection, value_ret)); actual_selection.clear(); selection.clear(); @@ -196,7 +195,7 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) add_coin(5 * CENT, 5, actual_selection); add_coin(4 * CENT, 4, actual_selection); add_coin(1 * CENT, 1, actual_selection); - BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 10 * CENT, 0.5 * CENT, selection, value_ret, not_input_fees)); + BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 10 * CENT, 0.5 * CENT, selection, value_ret)); BOOST_CHECK(equal_sets(selection, actual_selection)); BOOST_CHECK_EQUAL(value_ret, 10 * CENT); actual_selection.clear(); @@ -207,21 +206,21 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) add_coin(5 * CENT, 5, actual_selection); add_coin(3 * CENT, 3, actual_selection); add_coin(2 * CENT, 2, actual_selection); - BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 10 * CENT, 5000, selection, value_ret, not_input_fees)); + BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 10 * CENT, 5000, selection, value_ret)); BOOST_CHECK_EQUAL(value_ret, 10 * CENT); // FIXME: this test is redundant with the above, because 1 Cent is selected, not "too small" // BOOST_CHECK(equal_sets(selection, actual_selection)); // Select 0.25 Cent, not possible - BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 0.25 * CENT, 0.5 * CENT, selection, value_ret, not_input_fees)); + BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 0.25 * CENT, 0.5 * CENT, selection, value_ret)); actual_selection.clear(); selection.clear(); // Iteration exhaustion test CAmount target = make_hard_case(17, utxo_pool); - BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), target, 0, selection, value_ret, not_input_fees)); // Should exhaust + BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), target, 0, selection, value_ret)); // Should exhaust target = make_hard_case(14, utxo_pool); - BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), target, 0, selection, value_ret, not_input_fees)); // Should not exhaust + BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), target, 0, selection, value_ret)); // Should not exhaust // Test same value early bailout optimization utxo_pool.clear(); @@ -238,7 +237,7 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) for (int i = 0; i < 50000; ++i) { add_coin(5 * CENT, 7, utxo_pool); } - BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 30 * CENT, 5000, selection, value_ret, not_input_fees)); + BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 30 * CENT, 5000, selection, value_ret)); BOOST_CHECK_EQUAL(value_ret, 30 * CENT); BOOST_CHECK(equal_sets(selection, actual_selection)); @@ -252,11 +251,11 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) } // Run 100 times, to make sure it is never finding a solution for (int i = 0; i < 100; ++i) { - BOOST_CHECK(!SelectCoinsBnB(GroupCoins(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)); } // Make sure that effective value is working in SelectCoinsMinConf when BnB is used - CoinSelectionParams coin_selection_params_bnb(/* use_bnb= */ true, /* change_output_size= */ 0, + CoinSelectionParams coin_selection_params_bnb(/* change_output_size= */ 0, /* change_spend_size= */ 0, /* effective_feerate= */ CFeeRate(3000), /* long_term_feerate= */ CFeeRate(1000), /* discard_feerate= */ CFeeRate(1000), /* tx_no_inputs_size= */ 0, /* avoid_partial= */ false); @@ -266,21 +265,20 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) wallet->LoadWallet(); LOCK(wallet->cs_wallet); - bool bnb_used; std::vector coins; CoinSet setCoinsRet; CAmount nValueRet; add_coin(coins, *wallet, 1); coins.at(0).nInputBytes = 40; // Make sure that it has a negative effective value. The next check should assert if this somehow got through. Otherwise it will fail - BOOST_CHECK(!wallet->SelectCoinsMinConf(1 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params_bnb, bnb_used)); + BOOST_CHECK(!wallet->SelectCoinsMinConf(1 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params_bnb)); // Test fees subtracted from output: coins.clear(); add_coin(coins, *wallet, 1 * CENT); coins.at(0).nInputBytes = 40; coin_selection_params_bnb.m_subtract_fee_outputs = true; - BOOST_CHECK(wallet->SelectCoinsMinConf(1 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params_bnb, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf(1 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params_bnb)); BOOST_CHECK_EQUAL(nValueRet, 1 * CENT); } @@ -290,7 +288,6 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) wallet->SetupLegacyScriptPubKeyMan(); LOCK(wallet->cs_wallet); - bool bnb_used; std::vector coins; CoinSet setCoinsRet; CAmount nValueRet; @@ -302,9 +299,7 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) coin_control.fAllowOtherInputs = true; coin_control.Select(COutPoint(coins.at(0).tx->GetHash(), coins.at(0).i)); coin_selection_params_bnb.m_effective_feerate = CFeeRate(0); - BOOST_CHECK(wallet->SelectCoins(coins, 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_CHECK(wallet->SelectCoins(coins, 10 * CENT, setCoinsRet, nValueRet, coin_control, coin_selection_params_bnb)); } } @@ -317,7 +312,6 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) CoinSet setCoinsRet, setCoinsRet2; CAmount nValueRet; - bool bnb_used; std::vector coins; // test multiple times to allow for differences in the shuffle order @@ -326,24 +320,24 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) coins.clear(); // with an empty wallet we can't even pay one cent - BOOST_CHECK(!wallet->SelectCoinsMinConf( 1 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(!wallet->SelectCoinsMinConf( 1 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params)); add_coin(coins, *wallet, 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, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(!wallet->SelectCoinsMinConf( 1 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params)); // but we can find a new 1 cent - BOOST_CHECK(wallet->SelectCoinsMinConf( 1 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf( 1 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 1 * CENT); add_coin(coins, *wallet, 2*CENT); // add a mature 2 cent coin // we can't make 3 cents of mature coins - BOOST_CHECK(!wallet->SelectCoinsMinConf( 3 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(!wallet->SelectCoinsMinConf( 3 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params)); // we can make 3 cents of new coins - BOOST_CHECK(wallet->SelectCoinsMinConf( 3 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf( 3 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 3 * CENT); add_coin(coins, *wallet, 5*CENT); // add a mature 5 cent coin, @@ -353,33 +347,33 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) // 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, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(!wallet->SelectCoinsMinConf(38 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params)); // 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, filter_standard_extra, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(!wallet->SelectCoinsMinConf(38 * CENT, filter_standard_extra, coins, setCoinsRet, nValueRet, coin_selection_params)); // but we can make 37 cents if we accept new coins from ourself - BOOST_CHECK(wallet->SelectCoinsMinConf(37 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf(37 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 37 * CENT); // and we can make 38 cents if we accept all new coins - BOOST_CHECK(wallet->SelectCoinsMinConf(38 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf(38 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); 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, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf(34 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 35 * CENT); // but 35 cents is closest BOOST_CHECK_EQUAL(setCoinsRet.size(), 3U); // 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. We should see just 2+5 - BOOST_CHECK(wallet->SelectCoinsMinConf( 7 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf( 7 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); 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(wallet->SelectCoinsMinConf( 8 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf( 8 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); 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(wallet->SelectCoinsMinConf( 9 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf( 9 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 10 * CENT); BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); @@ -393,30 +387,30 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) add_coin(coins, *wallet, 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, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); - BOOST_CHECK(!wallet->SelectCoinsMinConf(72 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf(71 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(!wallet->SelectCoinsMinConf(72 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); // 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, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf(16 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 20 * CENT); // we should get 20 in one coin BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); add_coin(coins, *wallet, 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, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf(16 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 18 * CENT); // we should get 18 in 3 coins BOOST_CHECK_EQUAL(setCoinsRet.size(), 3U); add_coin(coins, *wallet, 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, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf(16 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 18 * CENT); // we should get 18 in 1 coin BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); // because in the event of a tie, the biggest coin wins // now try making 11 cents. we should get 5+6 - BOOST_CHECK(wallet->SelectCoinsMinConf(11 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf(11 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 11 * CENT); BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U); @@ -425,11 +419,11 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) add_coin(coins, *wallet, 2*COIN); add_coin(coins, *wallet, 3*COIN); add_coin(coins, *wallet, 4*COIN); // now we have 5+6+7+8+18+20+30+100+200+300+400 = 1094 cents - BOOST_CHECK(wallet->SelectCoinsMinConf(95 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf(95 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 1 * COIN); // we should get 1 BTC in 1 coin BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); - BOOST_CHECK(wallet->SelectCoinsMinConf(195 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf(195 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 2 * COIN); // we should get 2 BTC in 1 coin BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); @@ -444,14 +438,14 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) // 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(wallet->SelectCoinsMinConf(MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf(MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, MIN_CHANGE); // but if we add a bigger coin, small change is avoided add_coin(coins, *wallet, 1111*MIN_CHANGE); // try making 1 from 0.1 + 0.2 + 0.3 + 0.4 + 0.5 + 1111 = 1112.5 - BOOST_CHECK(wallet->SelectCoinsMinConf(1 * MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf(1 * MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 1 * MIN_CHANGE); // we should get the exact amount // if we add more small coins: @@ -459,7 +453,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) add_coin(coins, *wallet, MIN_CHANGE * 7 / 10); // and try again to make 1.0 * MIN_CHANGE - BOOST_CHECK(wallet->SelectCoinsMinConf(1 * MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf(1 * MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 1 * MIN_CHANGE); // we should get the exact amount // run the 'mtgox' test (see https://blockexplorer.com/tx/29a3efd3ef04f9153d47a990bd7b048a4b2d213daaa5fb8ed670fb85f13bdbcf) @@ -468,7 +462,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) for (int j = 0; j < 20; j++) add_coin(coins, *wallet, 50000 * COIN); - BOOST_CHECK(wallet->SelectCoinsMinConf(500000 * COIN, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf(500000 * COIN, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 500000 * COIN); // we should get the exact amount BOOST_CHECK_EQUAL(setCoinsRet.size(), 10U); // in ten coins @@ -481,7 +475,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) add_coin(coins, *wallet, MIN_CHANGE * 6 / 10); add_coin(coins, *wallet, MIN_CHANGE * 7 / 10); add_coin(coins, *wallet, 1111 * MIN_CHANGE); - BOOST_CHECK(wallet->SelectCoinsMinConf(1 * MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf(1 * MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 1111 * MIN_CHANGE); // we get the bigger coin BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); @@ -491,7 +485,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) add_coin(coins, *wallet, MIN_CHANGE * 6 / 10); add_coin(coins, *wallet, MIN_CHANGE * 8 / 10); add_coin(coins, *wallet, 1111 * MIN_CHANGE); - BOOST_CHECK(wallet->SelectCoinsMinConf(MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf(MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, MIN_CHANGE); // we should get the exact amount BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U); // in two coins 0.4+0.6 @@ -502,12 +496,12 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) add_coin(coins, *wallet, MIN_CHANGE * 100); // trying to make 100.01 from these three coins - BOOST_CHECK(wallet->SelectCoinsMinConf(MIN_CHANGE * 10001 / 100, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf(MIN_CHANGE * 10001 / 100, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, MIN_CHANGE * 10105 / 100); // we should get all coins 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(wallet->SelectCoinsMinConf(MIN_CHANGE * 9990 / 100, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf(MIN_CHANGE * 9990 / 100, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 101 * MIN_CHANGE); BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U); } @@ -521,7 +515,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) // We only create the wallet once to save time, but we still run the coin selection RUN_TESTS times. for (int i = 0; i < RUN_TESTS; i++) { - BOOST_CHECK(wallet->SelectCoinsMinConf(2000, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf(2000, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); if (amt - 2000 < MIN_CHANGE) { // needs more than one input: @@ -547,17 +541,19 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) for (int i = 0; i < RUN_TESTS; i++) { // picking 50 from 100 coins doesn't depend on the shuffle, // but does depend on randomness in the stochastic approximation code - BOOST_CHECK(wallet->SelectCoinsMinConf(50 * COIN, filter_standard, coins, setCoinsRet , nValueRet, coin_selection_params, bnb_used)); - BOOST_CHECK(wallet->SelectCoinsMinConf(50 * COIN, filter_standard, coins, setCoinsRet2, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(KnapsackSolver(50 * COIN, GroupCoins(coins), setCoinsRet, nValueRet, /*fFullyMixedOnly=*/false, /*maxTxFee=*/DEFAULT_TRANSACTION_MAXFEE)); + BOOST_CHECK(KnapsackSolver(50 * COIN, GroupCoins(coins), setCoinsRet2, nValueRet, /*fFullyMixedOnly=*/false, /*maxTxFee=*/DEFAULT_TRANSACTION_MAXFEE)); 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(wallet->SelectCoinsMinConf(COIN, filter_standard, coins, setCoinsRet , nValueRet, coin_selection_params, bnb_used)); - BOOST_CHECK(wallet->SelectCoinsMinConf(COIN, filter_standard, coins, setCoinsRet2, nValueRet, coin_selection_params, bnb_used)); + // Test that the KnapsackSolver selects randomly from equivalent coins (same value and same input size). + // When choosing 1 from 100 identical coins, 1% of the time, this test will choose the same coin twice + // which will cause it to fail. + // To avoid that issue, run the test RANDOM_REPEATS times and only complain if all of them fail + BOOST_CHECK(KnapsackSolver(COIN, GroupCoins(coins), setCoinsRet, nValueRet, /*fFullyMixedOnly=*/false, /*maxTxFee=*/DEFAULT_TRANSACTION_MAXFEE)); + BOOST_CHECK(KnapsackSolver(COIN, GroupCoins(coins), setCoinsRet2, nValueRet, /*fFullyMixedOnly=*/false, /*maxTxFee=*/DEFAULT_TRANSACTION_MAXFEE)); if (equal_sets(setCoinsRet, setCoinsRet2)) fails++; } @@ -577,10 +573,8 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) 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(wallet->SelectCoinsMinConf(90*CENT, filter_standard, coins, setCoinsRet , nValueRet, coin_selection_params, bnb_used)); - BOOST_CHECK(wallet->SelectCoinsMinConf(90*CENT, filter_standard, coins, setCoinsRet2, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(KnapsackSolver(90*CENT, GroupCoins(coins), setCoinsRet, nValueRet, /*fFullyMixedOnly=*/false, /*maxTxFee=*/DEFAULT_TRANSACTION_MAXFEE)); + BOOST_CHECK(KnapsackSolver(90*CENT, GroupCoins(coins), setCoinsRet2, nValueRet, /*fFullyMixedOnly=*/false, /*maxTxFee=*/DEFAULT_TRANSACTION_MAXFEE)); if (equal_sets(setCoinsRet, setCoinsRet2)) fails++; } @@ -598,7 +592,6 @@ BOOST_AUTO_TEST_CASE(ApproximateBestSubset) CoinSet setCoinsRet; CAmount nValueRet; - bool bnb_used; std::vector coins; // Test vValue sort order @@ -606,7 +599,7 @@ BOOST_AUTO_TEST_CASE(ApproximateBestSubset) add_coin(coins, *wallet, 1000 * COIN); add_coin(coins, *wallet, 3 * COIN); - BOOST_CHECK(wallet->SelectCoinsMinConf(1003 * COIN, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); + BOOST_CHECK(wallet->SelectCoinsMinConf(1003 * COIN, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 1003 * COIN); BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U); } @@ -645,19 +638,14 @@ BOOST_AUTO_TEST_CASE(SelectCoins_test) CAmount target = rand.randrange(balance - 1000) + 1000; // Perform selection - CoinSelectionParams coin_selection_params_knapsack(/* use_bnb= */ false, /* change_output_size= */ 34, - /* change_spend_size= */ 148, /* effective_feerate= */ CFeeRate(0), - /* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0), - /* tx_no_inputs_size= */ 0, /* avoid_partial= */ false); - CoinSelectionParams coin_selection_params_bnb(/* use_bnb= */ true, /* change_output_size= */ 34, - /* change_spend_size= */ 148, /* effective_feerate= */ CFeeRate(0), - /* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0), - /* tx_no_inputs_size= */ 0, /* avoid_partial= */ false); + CoinSelectionParams cs_params(/* change_output_size= */ 34, + /* change_spend_size= */ 148, /* effective_feerate= */ CFeeRate(0), + /* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0), + /* tx_no_inputs_size= */ 0, /* avoid_partial= */ false); CoinSet out_set; CAmount out_value = 0; - bool bnb_used = false; - BOOST_CHECK(wallet->SelectCoinsMinConf(target, filter_standard, coins, out_set, out_value, coin_selection_params_bnb, bnb_used) || - wallet->SelectCoinsMinConf(target, filter_standard, coins, out_set, out_value, coin_selection_params_knapsack, bnb_used)); + CCoinControl cc; + BOOST_CHECK(wallet->SelectCoins(coins, target, out_set, out_value, cc, cs_params)); BOOST_CHECK_GE(out_value, target); } } diff --git a/src/wallet/test/spend_tests.cpp b/src/wallet/test/spend_tests.cpp new file mode 100644 index 0000000000000..ad739c6cc0872 --- /dev/null +++ b/src/wallet/test/spend_tests.cpp @@ -0,0 +1,61 @@ +// Copyright (c) 2021 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 + +BOOST_FIXTURE_TEST_SUITE(spend_tests, WalletTestingSetup) + +BOOST_FIXTURE_TEST_CASE(SubtractFee, TestChain100Setup) +{ + CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); + auto wallet = CreateSyncedWallet(*m_node.chain, *m_node.coinjoin_loader, m_node.chainman->ActiveChain(), coinbaseKey); + + // Check that a subtract-from-recipient transaction slightly less than the + // coinbase input amount does not create a change output (because it would + // be uneconomical to add and spend the output), and make sure it pays the + // leftover input amount which would have been change to the recipient + // instead of the miner. + auto check_tx = [&wallet](CAmount leftover_input_amount) { + CRecipient recipient{GetScriptForRawPubKey({}), 500 * COIN - leftover_input_amount, true /* subtract fee */}; + CTransactionRef tx; + CAmount fee; + int change_pos = -1; + bilingual_str error; + CCoinControl coin_control; + coin_control.m_feerate.emplace(10000); + coin_control.fOverrideFeeRate = true; + FeeCalculation fee_calc; + BOOST_CHECK(wallet->CreateTransaction({recipient}, tx, fee, change_pos, error, coin_control, fee_calc)); + BOOST_CHECK_EQUAL(tx->vout.size(), 1); + BOOST_CHECK_EQUAL(tx->vout[0].nValue, recipient.nAmount + leftover_input_amount - fee); + BOOST_CHECK_GT(fee, 0); + return fee; + }; + + // Send full input amount to recipient, check that only nonzero fee is + // subtracted (to_reduce == fee). + const CAmount fee{check_tx(0)}; + + // Send slightly less than full input amount to recipient, check leftover + // input amount is paid to recipient not the miner (to_reduce == fee - 123) + BOOST_CHECK_EQUAL(fee, check_tx(123)); + + // Send full input minus fee amount to recipient, check leftover input + // amount is paid to recipient not the miner (to_reduce == 0) + BOOST_CHECK_EQUAL(fee, check_tx(fee)); + + // Send full input minus more than the fee amount to recipient, check + // leftover input amount is paid to recipient not the miner (to_reduce == + // -123). This overpays the recipient instead of overpaying the miner more + // than double the neccesary fee. + BOOST_CHECK_EQUAL(fee, check_tx(fee + 123)); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/wallet/test/util.cpp b/src/wallet/test/util.cpp new file mode 100644 index 0000000000000..e8015dfadb8aa --- /dev/null +++ b/src/wallet/test/util.cpp @@ -0,0 +1,38 @@ +// Copyright (c) 2021 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 + +std::unique_ptr CreateSyncedWallet(interfaces::Chain& chain, interfaces::CoinJoin::Loader& coinjoin_loader, CChain& cchain, const CKey& key) +{ + auto wallet = std::make_unique(&chain, &coinjoin_loader, "", CreateMockWalletDatabase()); + { + LOCK(wallet->cs_wallet); + wallet->SetLastBlockProcessed(cchain.Height(), cchain.Tip()->GetBlockHash()); + } + wallet->LoadWallet(); + { + auto spk_man = wallet->GetOrCreateLegacyScriptPubKeyMan(); + LOCK2(wallet->cs_wallet, spk_man->cs_KeyStore); + spk_man->AddKeyPubKey(key, key.GetPubKey()); + } + WalletRescanReserver reserver(*wallet); + reserver.reserve(); + CWallet::ScanResult result = wallet->ScanForWalletTransactions(cchain.Genesis()->GetBlockHash(), 0 /* start_height */, {} /* max_height */, reserver, false /* update */); + BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS); + BOOST_CHECK_EQUAL(result.last_scanned_block, cchain.Tip()->GetBlockHash()); + BOOST_CHECK_EQUAL(*result.last_scanned_height, cchain.Height()); + BOOST_CHECK(result.last_failed_block.IsNull()); + return wallet; +} diff --git a/src/wallet/test/util.h b/src/wallet/test/util.h new file mode 100644 index 0000000000000..ac3b8e74ef74a --- /dev/null +++ b/src/wallet/test/util.h @@ -0,0 +1,22 @@ +// Copyright (c) 2021 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_TEST_UTIL_H +#define BITCOIN_WALLET_TEST_UTIL_H + +#include + +class CChain; +class CKey; +class CWallet; +namespace interfaces { +class Chain; +namespace CoinJoin { +class Loader; +} //namespsace CoinJoin +} // namespace interfaces + +std::unique_ptr CreateSyncedWallet(interfaces::Chain& chain, interfaces::CoinJoin::Loader& coinjoin_loader, CChain& cchain, const CKey& key); + +#endif // BITCOIN_WALLET_TEST_UTIL_H diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index 7fa30ec727d52..80bd82defa1de 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -585,20 +586,7 @@ class ListCoinsTestingSetup : public TestChain100Setup ListCoinsTestingSetup() { CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); - wallet = std::make_unique(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateMockWalletDatabase()); - { - LOCK(wallet->cs_wallet); - wallet->SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); - } - wallet->LoadWallet(); - AddKey(*wallet, coinbaseKey); - WalletRescanReserver reserver(*wallet); - reserver.reserve(); - CWallet::ScanResult result = wallet->ScanForWalletTransactions(m_node.chainman->ActiveChain().Genesis()->GetBlockHash(), 0 /* start_height */, {} /* max_height */, reserver, false /* update */); - BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS); - BOOST_CHECK_EQUAL(result.last_scanned_block, m_node.chainman->ActiveChain().Tip()->GetBlockHash()); - BOOST_CHECK_EQUAL(*result.last_scanned_height, m_node.chainman->ActiveChain().Height()); - BOOST_CHECK(result.last_failed_block.IsNull()); + wallet = CreateSyncedWallet(*m_node.chain, *m_node.coinjoin_loader, m_node.chainman->ActiveChain(), coinbaseKey); } ~ListCoinsTestingSetup() @@ -1365,7 +1353,6 @@ static size_t CalculateNestedKeyhashInputSize(bool use_max_sig) return ::GetSerializeSize(tx_in, PROTOCOL_VERSION); } -static constexpr size_t DUMMY_NESTED_P2PKH_INPUT_SIZE = 113; BOOST_FIXTURE_TEST_CASE(dummy_input_size_test, TestChain100Setup) { BOOST_CHECK_EQUAL(CalculateNestedKeyhashInputSize(false), DUMMY_NESTED_P2PKH_INPUT_SIZE); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 4f6794f5ddc9c..371bc78a31760 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -2547,25 +2547,27 @@ CWallet::Balance CWallet::GetBalance(const int min_depth, const bool avoid_reuse { LOCK(cs_wallet); std::set trusted_parents; - for (auto pcoin : GetSpendableTXs()) { - const bool is_trusted{IsTrusted(*pcoin, trusted_parents)}; - const int tx_depth{pcoin->GetDepthInMainChain()}; - const CAmount tx_credit_mine{pcoin->GetAvailableCredit(/* fUseCache */ true, ISMINE_SPENDABLE | reuse_filter)}; - const CAmount tx_credit_watchonly{pcoin->GetAvailableCredit(/* fUseCache */ true, ISMINE_WATCH_ONLY | reuse_filter)}; - if (is_trusted && ((tx_depth >= min_depth) || (fAddLocked && pcoin->IsLockedByInstantSend()))) { + for (const auto* pcoin : GetSpendableTXs()) { + const auto& wtx{*pcoin}; + + const bool is_trusted{IsTrusted(*&wtx, trusted_parents)}; + const int tx_depth{wtx.GetDepthInMainChain()}; + const CAmount tx_credit_mine{wtx.GetAvailableCredit(/* fUseCache */ true, ISMINE_SPENDABLE | reuse_filter)}; + const CAmount tx_credit_watchonly{wtx.GetAvailableCredit(/* fUseCache */ true, ISMINE_WATCH_ONLY | reuse_filter)}; + if (is_trusted && ((tx_depth >= min_depth) || (fAddLocked && wtx.IsLockedByInstantSend()))) { ret.m_mine_trusted += tx_credit_mine; ret.m_watchonly_trusted += tx_credit_watchonly; } - if (!is_trusted && tx_depth == 0 && pcoin->InMempool()) { + if (!is_trusted && tx_depth == 0 && wtx.InMempool()) { ret.m_mine_untrusted_pending += tx_credit_mine; ret.m_watchonly_untrusted_pending += tx_credit_watchonly; } - ret.m_mine_immature += pcoin->GetImmatureCredit(); - ret.m_watchonly_immature += pcoin->GetImmatureWatchOnlyCredit(); + ret.m_mine_immature += wtx.GetImmatureCredit(); + ret.m_watchonly_immature += wtx.GetImmatureWatchOnlyCredit(); if (CCoinJoinClientOptions::IsEnabled()) { - ret.m_anonymized += pcoin->GetAnonymizedCredit(coinControl); - ret.m_denominated_trusted += pcoin->GetDenominatedCredit(false); - ret.m_denominated_untrusted_pending += pcoin->GetDenominatedCredit(true); + ret.m_anonymized += wtx.GetAnonymizedCredit(coinControl); + ret.m_denominated_trusted += wtx.GetDenominatedCredit(false); + ret.m_denominated_untrusted_pending += wtx.GetDenominatedCredit(true); } } } @@ -2671,23 +2673,25 @@ void CWallet::AvailableCoins(std::vector &vCoins, const CCoinControl* c const bool only_safe = {coinControl ? !coinControl->m_include_unsafe_inputs : true}; std::set trusted_parents; - for (auto pcoin : GetSpendableTXs()) { - const uint256& wtxid = pcoin->GetHash(); + for (const auto* pcoin : GetSpendableTXs()) { + const auto& wtx{*pcoin}; - if (!chain().checkFinalTx(*pcoin->tx)) + const uint256& wtxid = wtx.GetHash(); + + if (!chain().checkFinalTx(*wtx.tx)) continue; - if (pcoin->IsImmatureCoinBase()) + if (wtx.IsImmatureCoinBase()) continue; - int nDepth = pcoin->GetDepthInMainChain(); + int nDepth = wtx.GetDepthInMainChain(); // We should not consider coins which aren't at least in our mempool // It's possible for these to be conflicted via ancestors which we may never be able to detect - if (nDepth == 0 && !pcoin->InMempool()) + if (nDepth == 0 && !wtx.InMempool()) continue; - bool safeTx = IsTrusted(*pcoin, trusted_parents); + bool safeTx = IsTrusted(*&wtx, trusted_parents); if (only_safe && !safeTx) { continue; @@ -2697,31 +2701,31 @@ void CWallet::AvailableCoins(std::vector &vCoins, const CCoinControl* c continue; } - for (unsigned int i = 0; i < pcoin->tx->vout.size(); i++) { + for (unsigned int i = 0; i < wtx.tx->vout.size(); i++) { bool found = false; switch (nCoinType) { case CoinType::ONLY_FULLY_MIXED: { - found = CoinJoin::IsDenominatedAmount(pcoin->tx->vout[i].nValue) && + found = CoinJoin::IsDenominatedAmount(wtx.tx->vout[i].nValue) && IsFullyMixed(COutPoint(wtxid, i)); break; } case CoinType::ONLY_READY_TO_MIX: { - found = CoinJoin::IsDenominatedAmount(pcoin->tx->vout[i].nValue) && + found = CoinJoin::IsDenominatedAmount(wtx.tx->vout[i].nValue) && !IsFullyMixed(COutPoint(wtxid, i)); break; } case CoinType::ONLY_NONDENOMINATED: { // NOTE: do not use collateral amounts - found = !CoinJoin::IsCollateralAmount(pcoin->tx->vout[i].nValue) && - !CoinJoin::IsDenominatedAmount(pcoin->tx->vout[i].nValue); + found = !CoinJoin::IsCollateralAmount(wtx.tx->vout[i].nValue) && + !CoinJoin::IsDenominatedAmount(wtx.tx->vout[i].nValue); break; } case CoinType::ONLY_MASTERNODE_COLLATERAL: { - found = dmn_types::IsCollateralAmount(pcoin->tx->vout[i].nValue); + found = dmn_types::IsCollateralAmount(wtx.tx->vout[i].nValue); break; } case CoinType::ONLY_COINJOIN_COLLATERAL: { - found = CoinJoin::IsCollateralAmount(pcoin->tx->vout[i].nValue); + found = CoinJoin::IsCollateralAmount(wtx.tx->vout[i].nValue); break; } case CoinType::ALL_COINS: { @@ -2736,7 +2740,7 @@ void CWallet::AvailableCoins(std::vector &vCoins, const CCoinControl* c continue; } - if (pcoin->tx->vout[i].nValue < nMinimumAmount || pcoin->tx->vout[i].nValue > nMaximumAmount) + if (wtx.tx->vout[i].nValue < nMinimumAmount || wtx.tx->vout[i].nValue > nMaximumAmount) continue; if (coinControl && coinControl->HasSelected() && !coinControl->fAllowOtherInputs && !coinControl->IsSelected(COutPoint(wtxid, i))) @@ -2748,7 +2752,7 @@ void CWallet::AvailableCoins(std::vector &vCoins, const CCoinControl* c if (IsSpent(wtxid, i)) continue; - isminetype mine = IsMine(pcoin->tx->vout[i]); + isminetype mine = IsMine(wtx.tx->vout[i]); if (mine == ISMINE_NO) { continue; @@ -2758,16 +2762,16 @@ void CWallet::AvailableCoins(std::vector &vCoins, const CCoinControl* c continue; } - std::unique_ptr provider = GetSolvingProvider(pcoin->tx->vout[i].scriptPubKey); + std::unique_ptr provider = GetSolvingProvider(wtx.tx->vout[i].scriptPubKey); - bool solvable = provider ? IsSolvable(*provider, pcoin->tx->vout[i].scriptPubKey) : false; + bool solvable = provider ? IsSolvable(*provider, wtx.tx->vout[i].scriptPubKey) : false; bool spendable = ((mine & ISMINE_SPENDABLE) != ISMINE_NO) || (((mine & ISMINE_WATCH_ONLY) != ISMINE_NO) && (coinControl && coinControl->fAllowWatchOnly && solvable)); - vCoins.push_back(COutput(pcoin, i, nDepth, spendable, solvable, safeTx, (coinControl && coinControl->fAllowWatchOnly))); + vCoins.push_back(COutput(&wtx, i, nDepth, spendable, solvable, safeTx, (coinControl && coinControl->fAllowWatchOnly))); // Checks the sum amount of all UTXO's. if (nMinimumSumAmount != MAX_MONEY) { - nTotal += pcoin->tx->vout[i].nValue; + nTotal += wtx.tx->vout[i].nValue; if (nTotal >= nMinimumSumAmount) { return; @@ -2888,36 +2892,24 @@ static bool isGroupISLocked(const OutputGroup& group, interfaces::Chain& chain) } bool CWallet::SelectCoinsMinConf(const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, std::vector coins, - std::set& setCoinsRet, CAmount& nValueRet, const CoinSelectionParams& coin_selection_params, bool& bnb_used, CoinType nCoinType) const + std::set& setCoinsRet, CAmount& nValueRet, const CoinSelectionParams& coin_selection_params, CoinType nCoinType) const { setCoinsRet.clear(); nValueRet = 0; - if (coin_selection_params.use_bnb) { - // Get the feerate for effective value. - // When subtracting the fee from the outputs, we want the effective feerate to be 0 - CFeeRate effective_feerate{0}; - if (!coin_selection_params.m_subtract_fee_outputs) { - effective_feerate = coin_selection_params.m_effective_feerate; - } - - std::vector groups = GroupOutputs(coins, !coin_selection_params.m_avoid_partial_spends, effective_feerate, coin_selection_params.m_long_term_feerate, eligibility_filter, true /* positive_only */); - - // Calculate cost of change - CAmount cost_of_change = coin_selection_params.m_discard_feerate.GetFee(coin_selection_params.change_spend_size) + coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.change_output_size); - - // Calculate the fees for things that aren't inputs - CAmount not_input_fees = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.tx_noinputs_size); - bnb_used = true; - return SelectCoinsBnB(groups, nTargetValue, cost_of_change, setCoinsRet, nValueRet, not_input_fees); - } else { - std::vector groups = GroupOutputs(coins, !coin_selection_params.m_avoid_partial_spends, CFeeRate(0), CFeeRate(0), eligibility_filter, false /* positive_only */); - bnb_used = false; - return KnapsackSolver(nTargetValue, groups, setCoinsRet, nValueRet, nCoinType == CoinType::ONLY_FULLY_MIXED, m_default_max_tx_fee); + // Note that unlike KnapsackSolver, we do not include the fee for creating a change output as BnB will not create a change output. + std::vector positive_groups = GroupOutputs(coins, coin_selection_params, eligibility_filter, true /* positive_only */); + if (SelectCoinsBnB(positive_groups, nTargetValue, coin_selection_params.m_cost_of_change, setCoinsRet, nValueRet)) { + return true; } + // The knapsack solver has some legacy behavior where it will spend dust outputs. We retain this behavior, so don't filter for positive only here. + std::vector all_groups = GroupOutputs(coins, coin_selection_params, eligibility_filter, false /* positive_only */); + // While nTargetValue includes the transaction fees for non-input things, it does not include the fee for creating a change output. + // So we need to include that for KnapsackSolver as well, as we are expecting to create a change output. + return KnapsackSolver(nTargetValue + coin_selection_params.m_change_fee, all_groups, setCoinsRet, nValueRet, nCoinType == CoinType::ONLY_FULLY_MIXED, m_default_max_tx_fee); } -bool CWallet::SelectCoins(const std::vector& vAvailableCoins, const CAmount& nTargetValue, std::set& setCoinsRet, CAmount& nValueRet, const CCoinControl& coin_control, CoinSelectionParams& coin_selection_params, bool& bnb_used) const +bool CWallet::SelectCoins(const std::vector& vAvailableCoins, const CAmount& nTargetValue, std::set& setCoinsRet, CAmount& nValueRet, const CCoinControl& coin_control, CoinSelectionParams& coin_selection_params) const { // Note: this function should never be used for "always free" tx types like dstx @@ -2925,9 +2917,6 @@ bool CWallet::SelectCoins(const std::vector& vAvailableCoins, const CAm CoinType nCoinType = coin_control.nCoinType; CAmount value_to_select = nTargetValue; - // Default to bnb was not used. If we use it, we set it later - bnb_used = false; - // coin control -> return all selected outputs (we want all selected to go into the transaction for sure) if (coin_control.HasSelected() && !coin_control.fAllowOtherInputs) { @@ -2959,9 +2948,9 @@ bool CWallet::SelectCoins(const std::vector& vAvailableCoins, const CAm std::map::const_iterator it = mapWallet.find(outpoint.hash); if (it != mapWallet.end()) { - const CWalletTx* pcoin = &it->second; + const CWalletTx& wtx = it->second; // Clearly invalid input, fail - if (pcoin->tx->vout.size() <= outpoint.n) { + if (wtx.tx->vout.size() <= outpoint.n) { return false; } if (nCoinType == CoinType::ONLY_FULLY_MIXED) { @@ -2970,16 +2959,16 @@ bool CWallet::SelectCoins(const std::vector& vAvailableCoins, const CAm if (!IsFullyMixed(outpoint)) continue; } // Just to calculate the marginal byte size - CInputCoin coin(pcoin->tx, outpoint.n, pcoin->GetSpendSize(outpoint.n, false)); + CInputCoin coin(wtx.tx, outpoint.n, wtx.GetSpendSize(outpoint.n, false)); nValueFromPresetInputs += coin.txout.nValue; if (coin.m_input_bytes <= 0) { return false; // Not solvable, can't estimate size for fee } coin.effective_value = coin.txout.nValue - coin_selection_params.m_effective_feerate.GetFee(coin.m_input_bytes); - if (coin_selection_params.use_bnb) { - value_to_select -= coin.effective_value; - } else { + if (coin_selection_params.m_subtract_fee_outputs) { value_to_select -= coin.txout.nValue; + } else { + value_to_select -= coin.effective_value; } setPresetCoins.insert(coin); } else { @@ -3020,26 +3009,26 @@ bool CWallet::SelectCoins(const std::vector& vAvailableCoins, const CAm // If possible, fund the transaction with confirmed UTXOs only. Prefer at least six // confirmations on outputs received from other wallets and only spend confirmed change. - if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(1, 6, 0), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used, nCoinType)) return true; - if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(1, 1, 0), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used, nCoinType)) return true; + if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(1, 6, 0), vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) return true; + if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(1, 1, 0), vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) return true; // Fall back to using zero confirmation change (but with as few ancestors in the mempool as // possible) if we cannot fund the transaction otherwise. if (m_spend_zero_conf_change) { - if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, 2), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used, nCoinType)) return true; + if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, 2), vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) return true; if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, std::min((size_t)4, max_ancestors/3), std::min((size_t)4, max_descendants/3)), - vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used, nCoinType)) { + vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) { return true; } if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, max_ancestors/2, max_descendants/2), - vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used, nCoinType)) { + vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) { return true; } // If partial groups are allowed, relax the requirement of spending OutputGroups (groups // of UTXOs sent to the same address, which are obviously controlled by a single wallet) // in their entirety. if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, max_ancestors-1, max_descendants-1, true /* include_partial_groups */), - vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used, nCoinType)) { + vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) { return true; } // Try with unsafe inputs if they are allowed. This may spend unconfirmed outputs @@ -3047,7 +3036,7 @@ bool CWallet::SelectCoins(const std::vector& vAvailableCoins, const CAm if (coin_control.m_include_unsafe_inputs && SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0 /* conf_mine */, 0 /* conf_theirs */, max_ancestors-1, max_descendants-1, true /* include_partial_groups */), - vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used, nCoinType)) { + vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) { return true; } // Try with unlimited ancestors/descendants. The transaction will still need to meet @@ -3055,7 +3044,7 @@ bool CWallet::SelectCoins(const std::vector& vAvailableCoins, const CAm // OutputGroups use heuristics that may overestimate ancestor/descendant counts. if (!fRejectLongChains && SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, std::numeric_limits::max(), std::numeric_limits::max(), true /* include_partial_groups */), - vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used, nCoinType)) { + vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) { return true; } } @@ -3542,7 +3531,7 @@ bool CWallet::CreateTransactionInternal( { CAmount nValue = 0; ReserveDestination reservedest(this); - int nChangePosRequest = nChangePosInOut; + const bool sort_bip69{nChangePosInOut == -1}; unsigned int nSubtractFeeFromAmount = 0; for (const auto& recipient : vecSend) { @@ -3564,35 +3553,16 @@ bool CWallet::CreateTransactionInternal( CMutableTransaction txNew; FeeCalculation feeCalc; - - CoinSelectionParams coin_selection_params; // Parameters for coin selection, init with dummy - coin_selection_params.m_discard_feerate = coin_control.m_discard_feerate ? *coin_control.m_discard_feerate : GetDiscardRate(*this); - - // Get the fee rate to use effective values in coin selection - coin_selection_params.m_effective_feerate = GetMinimumFeeRate(*this, coin_control, &feeCalc); - // Do not, ever, assume that it's fine to change the fee rate if the user has explicitly - // provided one - if (coin_control.m_feerate && coin_selection_params.m_effective_feerate > *coin_control.m_feerate) { - error = strprintf(_("Fee rate (%s) is lower than the minimum fee rate setting (%s)"), coin_control.m_feerate->ToString(FeeEstimateMode::DUFF_B), coin_selection_params.m_effective_feerate.ToString(FeeEstimateMode::DUFF_B)); - return false; - } - int nBytes{0}; + CAmount fee_needed{0}; { - std::vector vecCoins; + std::set setCoins; LOCK(cs_wallet); txNew.nLockTime = GetLocktimeForNewTransaction(chain(), GetLastBlockHash(), GetLastBlockHeight()); { - CAmount nAmountAvailable{0}; std::vector vAvailableCoins; AvailableCoins(vAvailableCoins, &coin_control, 1, MAX_MONEY, MAX_MONEY, 0); - coin_selection_params.use_bnb = false; // never use BnB - - for (auto out : vAvailableCoins) { - if (out.fSpendable) { - nAmountAvailable += out.tx->tx->vout[out.i].nValue; - } - } + CoinSelectionParams coin_selection_params; // Parameters for coin selection, init with dummy coin_selection_params.m_avoid_partial_spends = coin_control.m_avoid_partial_spends; // Create change script that will be used if we need change @@ -3623,253 +3593,211 @@ bool CWallet::CreateTransactionInternal( // change keypool ran out, but change is required. CHECK_NONFATAL(IsValidDestination(dest) != scriptChange.empty()); } + CTxOut change_prototype_txout(0, scriptChange); + coin_selection_params.change_output_size = GetSerializeSize(change_prototype_txout); + + // Get size of spending the change output + int change_spend_size = CalculateMaximumSignedInputSize(change_prototype_txout, this); + // If the wallet doesn't know how to sign change output, assume p2sh-p2pkh + // as lower-bound to allow BnB to do it's thing + if (change_spend_size == -1) { + coin_selection_params.change_spend_size = DUMMY_NESTED_P2PKH_INPUT_SIZE; + } else { + coin_selection_params.change_spend_size = (size_t)change_spend_size; + } - nFeeRet = 0; - bool pick_new_inputs = true; - CAmount nValueIn = 0; - CAmount nAmountToSelectAdditional{0}; - // Start with nAmountToSelectAdditional=0 and loop until there is enough to cover the request + fees, try it 500 times. - int nMaxTries = 500; - while (--nMaxTries > 0) - { - nChangePosInOut = std::numeric_limits::max(); - txNew.vin.clear(); - txNew.vout.clear(); - bool fFirst = true; - - CAmount nValueToSelect = nValue; - if (nSubtractFeeFromAmount == 0) { - assert(nAmountToSelectAdditional >= 0); - nValueToSelect += nAmountToSelectAdditional; - } - // vouts to the payees - for (const auto& recipient : vecSend) - { - CTxOut txout(recipient.nAmount, recipient.scriptPubKey); + // Set discard feerate + coin_selection_params.m_discard_feerate = coin_control.m_discard_feerate ? *coin_control.m_discard_feerate : GetDiscardRate(*this); - if (recipient.fSubtractFeeFromAmount) - { - assert(nSubtractFeeFromAmount != 0); - txout.nValue -= nFeeRet / nSubtractFeeFromAmount; // Subtract fee equally from each selected recipient + // Get the fee rate to use effective values in coin selection + coin_selection_params.m_effective_feerate = GetMinimumFeeRate(*this, coin_control, &feeCalc); + // Do not, ever, assume that it's fine to change the fee rate if the user has explicitly + // provided one + if (coin_control.m_feerate && coin_selection_params.m_effective_feerate > *coin_control.m_feerate) { + error = strprintf(_("Fee rate (%s) is lower than the minimum fee rate setting (%s)"), coin_control.m_feerate->ToString(FeeEstimateMode::DUFF_B), coin_selection_params.m_effective_feerate.ToString(FeeEstimateMode::DUFF_B)); + return false; + } + if (feeCalc.reason == FeeReason::FALLBACK && !m_allow_fallback_fee) { + // eventually allow a fallback fee + error = _("Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable -fallbackfee."); + return false; + } - if (fFirst) // first receiver pays the remainder not divisible by output count - { - fFirst = false; - txout.nValue -= nFeeRet % nSubtractFeeFromAmount; - } - } + // Get long term estimate + CCoinControl cc_temp; + cc_temp.m_confirm_target = chain().estimateMaxBlocks(); + coin_selection_params.m_long_term_feerate = GetMinimumFeeRate(*this, cc_temp, nullptr); + + // Calculate the cost of change + // Cost of change is the cost of creating the change output + cost of spending the change output in the future. + // For creating the change output now, we use the effective feerate. + // For spending the change output in the future, we use the discard feerate for now. + // So cost of change = (change output size * effective feerate) + (size of spending change output * discard feerate) + coin_selection_params.m_change_fee = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.change_output_size); + coin_selection_params.m_cost_of_change = coin_selection_params.m_discard_feerate.GetFee(coin_selection_params.change_spend_size) + coin_selection_params.m_change_fee; + + coin_selection_params.m_subtract_fee_outputs = nSubtractFeeFromAmount != 0; // If we are doing subtract fee from recipient, don't use effective values + + // vouts to the payees + if (!coin_selection_params.m_subtract_fee_outputs) { + coin_selection_params.tx_noinputs_size = 9; // Static vsize overhead + outputs vsize. 4 nVersion, 4 nLocktime, 1 input count + coin_selection_params.tx_noinputs_size += GetSizeOfCompactSize(vecSend.size()); // bytes for output count + } + for (const auto& recipient : vecSend) + { + CTxOut txout(recipient.nAmount, recipient.scriptPubKey); - if (IsDust(txout, chain().relayDustFee())) - { - if (recipient.fSubtractFeeFromAmount && nFeeRet > 0) - { - if (txout.nValue < 0) - error = _("The transaction amount is too small to pay the fee"); - else - error = _("The transaction amount is too small to send after the fee has been deducted"); - } - else - error = _("Transaction amount too small"); - return false; - } - txNew.vout.push_back(txout); + // Include the fee cost for outputs. + if (!coin_selection_params.m_subtract_fee_outputs) { + coin_selection_params.tx_noinputs_size += ::GetSerializeSize(txout, PROTOCOL_VERSION); } - // Choose coins to use - bool bnb_used = false; - if (pick_new_inputs) { - nValueIn = 0; - std::set setCoinsTmp; - if (!SelectCoins(vAvailableCoins, nValueToSelect, setCoinsTmp, nValueIn, coin_control, coin_selection_params, bnb_used)) { - if (coin_control.nCoinType == CoinType::ONLY_NONDENOMINATED) { - error = _("Unable to locate enough non-denominated funds for this transaction."); - } else if (coin_control.nCoinType == CoinType::ONLY_FULLY_MIXED) { - error = _("Unable to locate enough mixed funds for this transaction."); - error = error + Untranslated(" ") + strprintf(_("%s uses exact denominated amounts to send funds, you might simply need to mix some more coins."), gCoinJoinName); - } else if (nValueIn < nValueToSelect) { - error = _("Insufficient funds."); - } - return false; - } - vecCoins.assign(setCoinsTmp.begin(), setCoinsTmp.end()); + if (IsDust(txout, chain().relayDustFee())) + { + error = _("Transaction amount too small"); + return false; } + txNew.vout.push_back(txout); + } - // Fill vin - // - // Note how the sequence number is set to max()-1 so that the - // nLockTime set above actually works. - txNew.vin.clear(); - for (const auto& coin : vecCoins) { - txNew.vin.emplace_back(coin.outpoint, CScript(), CTxIn::SEQUENCE_FINAL - 1); + // Include the fees for things that aren't inputs, excluding the change output + const CAmount not_input_fees = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.tx_noinputs_size); + CAmount nValueToSelect = nValue + not_input_fees; + + // Choose coins to use + CAmount inputs_sum = 0; + setCoins.clear(); + if (!SelectCoins(vAvailableCoins, /* nTargetValue */ nValueToSelect, setCoins, inputs_sum, coin_control, coin_selection_params)) { + if (coin_control.nCoinType == CoinType::ONLY_NONDENOMINATED) { + error = _("Unable to locate enough non-denominated funds for this transaction."); + } else if (coin_control.nCoinType == CoinType::ONLY_FULLY_MIXED) { + error = _("Unable to locate enough mixed funds for this transaction."); + error = error + Untranslated(" ") + strprintf(_("%s uses exact denominated amounts to send funds, you might simply need to mix some more coins."), gCoinJoinName); + } else { + error = _("Insufficient funds."); } + return false; + } - auto calculateFee = [&](CAmount& nFee) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) -> bool { - AssertLockHeld(cs_wallet); - nBytes = CalculateMaximumSignedTxSize(CTransaction(txNew), this, coin_control.fAllowWatchOnly); - if (nBytes < 0) { - error = _("Signing transaction failed"); - return false; - } - - if (nExtraPayloadSize != 0) { - // account for extra payload in fee calculation - nBytes += GetSizeOfCompactSize(nExtraPayloadSize) + nExtraPayloadSize; - } + // Always make a change output + // We will reduce the fee from this change output later, and remove the output if it is too small. + const CAmount change_and_fee = inputs_sum - nValue; + assert(change_and_fee >= 0); + CTxOut newTxOut(change_and_fee, scriptChange); - if (static_cast(nBytes) > MAX_STANDARD_TX_SIZE) { - // Do not create oversized transactions (bad-txns-oversize). - error = _("Transaction too large"); - return false; - } + if (nChangePosInOut == -1) + { + // Insert change txn at random position: + nChangePosInOut = GetRandInt(txNew.vout.size()+1); + } + else if ((unsigned int)nChangePosInOut > txNew.vout.size()) + { + error = _("Transaction change output index out of range"); + return false; + } - // Remove scriptSigs to eliminate the fee calculation dummy signatures - for (auto& txin : txNew.vin) { - txin.scriptSig = CScript(); - } + assert(nChangePosInOut != -1); + auto change_position = txNew.vout.insert(txNew.vout.begin() + nChangePosInOut, newTxOut); + + // We're making a copy of vecSend because it's const, sortedVecSend should be used + // in place of vecSend in all subsequent usage. + std::vector sortedVecSend{vecSend}; + if (sort_bip69) { + std::sort(txNew.vout.begin(), txNew.vout.end(), CompareOutputBIP69()); + // The output reduction loop uses vecSend to map to txNew.vout, we need to + // shuffle them both to ensure this mapping remains consistent + std::sort(sortedVecSend.begin(), sortedVecSend.end(), + [](const CRecipient& a, const CRecipient& b) { + return a.nAmount < b.nAmount || (a.nAmount == b.nAmount && a.scriptPubKey < b.scriptPubKey); + }); + + // If there was a change output added before, we must update its position now + if (const auto it = std::find(txNew.vout.begin(), txNew.vout.end(), newTxOut); it != txNew.vout.end()) { + change_position = it; + nChangePosInOut = std::distance(txNew.vout.begin(), change_position); + } + }; - nFee = GetMinimumFee(*this, nBytes, coin_control, &feeCalc); + // Dummy fill vin for maximum size estimation + // + for (const auto& coin : setCoins) { + txNew.vin.push_back(CTxIn(coin.outpoint, CScript())); + } - return true; - }; + // Calculate the transaction fee + nBytes = CalculateMaximumSignedTxSize(CTransaction(txNew), this, coin_control.fAllowWatchOnly); + if (nBytes < 0) { + error = _("Signing transaction failed"); + return false; + } - if (!calculateFee(nFeeRet)) { - return false; - } + if (nExtraPayloadSize != 0) { + // account for extra payload in fee calculation + nBytes += GetSizeOfCompactSize(nExtraPayloadSize) + nExtraPayloadSize; + } - CTxOut newTxOut; - const CAmount nAmountLeft = nValueIn - nValue; - auto getChange = [&]() { - if (nSubtractFeeFromAmount > 0) { - return nAmountLeft; - } else { - return nAmountLeft - nFeeRet; - } - }; + fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes); - if (getChange() > 0) - { - //over pay for denominated transactions - if (coin_control.nCoinType == CoinType::ONLY_FULLY_MIXED) { - nChangePosInOut = -1; - nFeeRet += getChange(); - } else { - // Fill a vout to ourself with zero amount until we know the correct change - newTxOut = CTxOut(0, scriptChange); - txNew.vout.push_back(newTxOut); - - // Calculate the fee with the change output added, store the - // current fee to reset it in case the remainder is dust and we - // don't need to fee with change output added. - CAmount nFeePrev = nFeeRet; - if (!calculateFee(nFeeRet)) { - return false; - } + if (nSubtractFeeFromAmount == 0) { + change_position->nValue -= fee_needed; + } - // Remove the change output again, it will be added later again if required - txNew.vout.pop_back(); + // We want to drop the change to fees if: + // 1. The change output would be dust + // 2. The change is within the (almost) exact match window, i.e. it is less than or equal to the cost of the change output (cost_of_change) + // 3. We are working with fully mixed CoinJoin denominations + CAmount change_amount = change_position->nValue; + if (IsDust(*change_position, coin_selection_params.m_discard_feerate) || change_amount <= coin_selection_params.m_cost_of_change || coin_control.nCoinType == CoinType::ONLY_FULLY_MIXED) + { + nChangePosInOut = -1; + change_amount = 0; + txNew.vout.erase(change_position); - // Set the change amount properly - newTxOut.nValue = getChange(); + nBytes = CalculateMaximumSignedTxSize(CTransaction(txNew), this, coin_control.fAllowWatchOnly); + fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes); + } - // Never create dust outputs; if we would, just - // add the dust to the fee. - if (IsDust(newTxOut, coin_selection_params.m_discard_feerate)) - { - nFeeRet = nFeePrev; - nChangePosInOut = -1; - nFeeRet += getChange(); - } - else - { - if (nChangePosRequest == -1) - { - // Insert change txn at random position: - nChangePosInOut = GetRandInt(txNew.vout.size()+1); - } - else if ((unsigned int)nChangePosRequest > txNew.vout.size()) - { - error = _("Transaction change output index out of range"); - return false; - } else { - nChangePosInOut = nChangePosRequest; - } + nFeeRet = inputs_sum - nValue - change_amount; - std::vector::iterator position = txNew.vout.begin()+nChangePosInOut; - txNew.vout.insert(position, newTxOut); - } - } - } else { - nChangePosInOut = -1; - } + // Update nFeeRet in case fee_needed changed due to dropping the change output + if (fee_needed <= change_and_fee - change_amount) { + nFeeRet = change_and_fee - change_amount; + } - if (getChange() < 0) { - if (nSubtractFeeFromAmount == 0) { - // nValueIn is not enough to cover nValue + nFeeRet. Add the missing amount abs(nChange) to the fee - // and try to select other inputs in the next loop step to cover the full required amount. - nAmountToSelectAdditional += abs(getChange()); - } else if (nAmountToSelectAdditional > 0 && nValueToSelect == nAmountAvailable) { - // We tried selecting more and failed. We have no extra funds left, - // so just add 1 duff to fail in the next loop step with a correct reason - nAmountToSelectAdditional += 1; + // Reduce output values for subtractFeeFromAmount + if (nSubtractFeeFromAmount != 0) { + CAmount to_reduce = fee_needed + change_amount - change_and_fee; + int i = 0; + bool fFirst = true; + for (const auto& recipient : sortedVecSend) + { + if (i == nChangePosInOut) { + ++i; } - continue; - } + CTxOut& txout = txNew.vout[i]; - // If no specific change position was requested, apply BIP69 - if (nChangePosRequest == -1) { - std::sort(vecCoins.begin(), vecCoins.end(), CompareInputCoinBIP69()); - std::sort(txNew.vin.begin(), txNew.vin.end(), CompareInputBIP69()); - std::sort(txNew.vout.begin(), txNew.vout.end(), CompareOutputBIP69()); + if (recipient.fSubtractFeeFromAmount) + { + txout.nValue -= to_reduce / nSubtractFeeFromAmount; // Subtract fee equally from each selected recipient - // If there was a change output added before, we must update its position now - if (nChangePosInOut != -1) { - int i = 0; - for (const CTxOut& txOut : txNew.vout) + if (fFirst) // first receiver pays the remainder not divisible by output count { - if (txOut == newTxOut) - { - nChangePosInOut = i; - break; + fFirst = false; + txout.nValue -= to_reduce % nSubtractFeeFromAmount; + } + // Error if this output is reduced to be below dust + if (IsDust(txout, chain().relayDustFee())) { + if (txout.nValue < 0) { + error = _("The transaction amount is too small to pay the fee"); + } else { + error = _("The transaction amount is too small to send after the fee has been deducted"); } - i++; + return false; } } + ++i; } - - if (feeCalc.reason == FeeReason::FALLBACK && !m_allow_fallback_fee) { - // eventually allow a fallback fee - error = _("Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable -fallbackfee."); - return false; - } - - if (nAmountLeft == nFeeRet) { - // We either added the change amount to nFeeRet because the change amount was considered - // to be dust or the input exactly matches output + fee. - // Either way, we used the total amount of the inputs we picked and the transaction is ready. - break; - } - - // We have a change output and we don't need to subtruct fees, which means the transaction is ready. - if (nChangePosInOut != -1 && nSubtractFeeFromAmount == 0) { - break; - } - - // If subtracting fee from recipients, we now know what fee we - // need to subtract, we have no reason to reselect inputs - if (nSubtractFeeFromAmount > 0) { - // If we are in here the second time it means we already subtracted the fee from the - // output(s) and there weren't any issues while doing that. So the transaction is ready now - // and we can break. - if (!pick_new_inputs) { - break; - } - pick_new_inputs = false; - } - } - - if (nMaxTries == 0) { - error = _("Exceeded max tries."); - return false; + nFeeRet = fee_needed; } // Give up if change keypool ran out and change is required @@ -3878,8 +3806,17 @@ bool CWallet::CreateTransactionInternal( } } - // Make sure change position was updated one way or another - assert(nChangePosInOut != std::numeric_limits::max()); + // Fill in final vin and shuffle/sort it + txNew.vin.clear(); + + // Note how the sequence number is set to non-maxint so that + // the nLockTime set above actually works. + const uint32_t nSequence = CTxIn::SEQUENCE_FINAL - 1; + for (const auto& coin : setCoins) { + txNew.vin.push_back(CTxIn(coin.outpoint, CScript(), nSequence)); + } + if (sort_bip69) { std::sort(txNew.vin.begin(), txNew.vin.end(), CompareInputBIP69()); } + else { Shuffle(txNew.vin.begin(), txNew.vin.end(), FastRandomContext()); } if (sign && !SignTransaction(txNew)) { error = _("Signing transaction failed"); @@ -3888,6 +3825,18 @@ bool CWallet::CreateTransactionInternal( // Return the constructed transaction data. tx = MakeTransactionRef(std::move(txNew)); + + // Limit size + if ((sign && ::GetSerializeSize(*tx, PROTOCOL_VERSION) > MAX_STANDARD_TX_SIZE) || + (!sign && static_cast(nBytes) > MAX_STANDARD_TX_SIZE)) { + error = _("Transaction too large"); + return false; + } + } + + if (fee_needed > nFeeRet) { + error = _("Fee needed > fee paid"); + return false; } if (nFeeRet > m_default_max_tx_fee) { @@ -4270,27 +4219,27 @@ std::map CWallet::GetAddressBalances() const std::set trusted_parents; for (const auto& walletEntry : mapWallet) { - const CWalletTx *pcoin = &walletEntry.second; + const CWalletTx& wtx = walletEntry.second; - if (!IsTrusted(*pcoin, trusted_parents)) + if (!IsTrusted(*&wtx, trusted_parents)) continue; - if (pcoin->IsImmatureCoinBase()) + if (wtx.IsImmatureCoinBase()) continue; - int nDepth = pcoin->GetDepthInMainChain(); - if ((nDepth < (pcoin->IsFromMe(ISMINE_ALL) ? 0 : 1)) && !pcoin->IsLockedByInstantSend()) + int nDepth = wtx.GetDepthInMainChain(); + if ((nDepth < (wtx.IsFromMe(ISMINE_ALL) ? 0 : 1)) && !wtx.IsLockedByInstantSend()) continue; - for (unsigned int i = 0; i < pcoin->tx->vout.size(); i++) + for (unsigned int i = 0; i < wtx.tx->vout.size(); i++) { CTxDestination addr; - if (!IsMine(pcoin->tx->vout[i])) + if (!IsMine(wtx.tx->vout[i])) continue; - if(!ExtractDestination(pcoin->tx->vout[i].scriptPubKey, addr)) + if(!ExtractDestination(wtx.tx->vout[i].scriptPubKey, addr)) continue; - CAmount n = IsSpent(walletEntry.first, i) ? 0 : pcoin->tx->vout[i].nValue; + CAmount n = IsSpent(walletEntry.first, i) ? 0 : wtx.tx->vout[i].nValue; balances[addr] += n; } @@ -4308,13 +4257,13 @@ std::set< std::set > CWallet::GetAddressGroupings() const for (const auto& walletEntry : mapWallet) { - const CWalletTx *pcoin = &walletEntry.second; + const CWalletTx& wtx = walletEntry.second; - if (pcoin->tx->vin.size() > 0) + if (wtx.tx->vin.size() > 0) { bool any_mine = false; // group all input addresses with each other - for (const CTxIn& txin : pcoin->tx->vin) + for (const CTxIn& txin : wtx.tx->vin) { CTxDestination address; if(!IsMine(txin)) /* If this input isn't mine, ignore it */ @@ -4328,7 +4277,7 @@ std::set< std::set > CWallet::GetAddressGroupings() const // group change with input addresses if (any_mine) { - for (const CTxOut& txout : pcoin->tx->vout) + for (const CTxOut& txout : wtx.tx->vout) if (IsChange(txout)) { CTxDestination txoutAddr; @@ -4345,7 +4294,7 @@ std::set< std::set > CWallet::GetAddressGroupings() const } // group lone addrs by themselves - for (const auto& txout : pcoin->tx->vout) + for (const auto& txout : wtx.tx->vout) if (IsMine(txout)) { CTxDestination address; @@ -5521,12 +5470,12 @@ bool CWalletTx::IsImmatureCoinBase() const return GetBlocksToMaturity() > 0; } -std::vector CWallet::GroupOutputs(const std::vector& outputs, bool separate_coins, const CFeeRate& effective_feerate, const CFeeRate& long_term_feerate, const CoinEligibilityFilter& filter, bool positive_only) const +std::vector CWallet::GroupOutputs(const std::vector& outputs, const CoinSelectionParams& coin_sel_params, const CoinEligibilityFilter& filter, bool positive_only) const { std::vector groups_out; - if (separate_coins) { - // Single coin means no grouping. Each COutput gets its own OutputGroup. + if (!coin_sel_params.m_avoid_partial_spends) { + // Allowing partial spends means no grouping. Each COutput gets its own OutputGroup. for (const COutput& output : outputs) { // Skip outputs we cannot spend if (!output.fSpendable) continue; @@ -5536,11 +5485,11 @@ std::vector CWallet::GroupOutputs(const std::vector& outpu CInputCoin input_coin = output.GetInputCoin(); // Make an OutputGroup containing just this output - OutputGroup group{effective_feerate, long_term_feerate}; + OutputGroup group{coin_sel_params}; group.Insert(input_coin, output.nDepth, output.tx->IsFromMe(ISMINE_ALL), ancestors, descendants, positive_only); // Check the OutputGroup's eligibility. Only add the eligible ones. - if (positive_only && group.effective_value <= 0) continue; + if (positive_only && group.GetSelectionAmount() <= 0) continue; bool isISLocked = isGroupISLocked(group, chain()); if (group.m_outputs.size() > 0 && group.EligibleForSpending(filter, isISLocked)) groups_out.push_back(group); } @@ -5567,7 +5516,7 @@ std::vector CWallet::GroupOutputs(const std::vector& outpu if (groups.size() == 0) { // No OutputGroups for this scriptPubKey yet, add one - groups.emplace_back(effective_feerate, long_term_feerate); + groups.emplace_back(coin_sel_params); } // Get the last OutputGroup in the vector so that we can add the CInputCoin to it @@ -5578,7 +5527,7 @@ std::vector CWallet::GroupOutputs(const std::vector& outpu // to avoid surprising users with very high fees. if (group->m_outputs.size() >= OUTPUT_GROUP_MAX_ENTRIES) { // The last output group is full, add a new group to the vector and use that group for the insertion - groups.emplace_back(effective_feerate, long_term_feerate); + groups.emplace_back(coin_sel_params); group = &groups.back(); } @@ -5600,7 +5549,7 @@ std::vector CWallet::GroupOutputs(const std::vector& outpu } // Check the OutputGroup's eligibility. Only add the eligible ones. - if (positive_only && group.effective_value <= 0) continue; + if (positive_only && group.GetSelectionAmount() <= 0) continue; bool isISLocked = isGroupISLocked(group, chain()); if (group.m_outputs.size() > 0 && group.EligibleForSpending(filter, isISLocked)) groups_out.push_back(group); } diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index f2b8713d77d74..fe92535793943 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -101,6 +101,8 @@ static const CAmount DEFAULT_TRANSACTION_MAXFEE = COIN / 10; static const CAmount HIGH_TX_FEE_PER_KB = COIN / 100; //! -maxtxfee will warn if called with a higher fee than this amount (in satoshis) static const CAmount HIGH_MAX_TX_FEE = 100 * HIGH_TX_FEE_PER_KB; +//! Pre-calculated constants for input size estimation in *virtual size* +static constexpr size_t DUMMY_NESTED_P2PKH_INPUT_SIZE = 113; //! if set, all keys will be derived by using BIP39/BIP44 static const bool DEFAULT_USE_HD_WALLET = true; @@ -616,16 +618,6 @@ struct WalletTxHasher } }; -struct CompareInputCoinBIP69 -{ - inline bool operator()(const CInputCoin& a, const CInputCoin& b) const - { - // Note: CInputCoin-s are essentially inputs, their txouts are used for informational purposes only - // that's why we use CompareInputBIP69 to sort them in a BIP69 compliant way. - return CompareInputBIP69()(CTxIn(a.outpoint), CTxIn(b.outpoint)); - } -}; - class COutput { public: @@ -678,44 +670,6 @@ class COutput } }; -/** Parameters for one iteration of Coin Selection. */ -struct CoinSelectionParams -{ - /** Toggles use of Branch and Bound instead of Knapsack solver. */ - bool use_bnb = true; - /** Size of a change output in bytes, determined by the output type. */ - size_t change_output_size = 0; - /** Size of the input to spend a change output in virtual bytes. */ - size_t change_spend_size = 0; - /** The fee to spend these UTXOs at the long term feerate. */ - CFeeRate m_effective_feerate; - /** The feerate estimate used to estimate an upper bound on what should be sufficient to spend - * the change output sometime in the future. */ - CFeeRate m_long_term_feerate; - /** If the cost to spend a change output at the discard feerate exceeds its value, drop it to fees. */ - CFeeRate m_discard_feerate; - size_t tx_noinputs_size = 0; - /** Indicate that we are subtracting the fee from outputs */ - bool m_subtract_fee_outputs = false; - /** When true, always spend all (up to OUTPUT_GROUP_MAX_ENTRIES) or none of the outputs - * associated with the same address. This helps reduce privacy leaks resulting from address - * reuse. Dust outputs are not eligible to be added to output groups and thus not considered. */ - bool m_avoid_partial_spends = false; - - CoinSelectionParams(bool use_bnb, size_t change_output_size, size_t change_spend_size, CFeeRate effective_feerate, - CFeeRate long_term_feerate, CFeeRate discard_feerate, size_t tx_noinputs_size, bool avoid_partial) : - use_bnb(use_bnb), - change_output_size(change_output_size), - change_spend_size(change_spend_size), - m_effective_feerate(effective_feerate), - m_long_term_feerate(long_term_feerate), - m_discard_feerate(discard_feerate), - tx_noinputs_size(tx_noinputs_size), - m_avoid_partial_spends(avoid_partial) - {} - CoinSelectionParams() {} -}; - class WalletRescanReserver; //forward declarations for ScanForWalletTransactions/RescanFromTime /** * A CWallet maintains a set of transactions and balances, and provides the ability to create new transactions. @@ -889,7 +843,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati * from coin_control and Coin Selection if successful. */ bool SelectCoins(const std::vector& vAvailableCoins, const CAmount& nTargetValue, std::set& setCoinsRet, CAmount& nValueRet, - const CCoinControl& coin_control, CoinSelectionParams& coin_selection_params, bool& bnb_used) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + const CCoinControl& coin_control, CoinSelectionParams& coin_selection_params) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); /** Get a name for this wallet for logging/debugging purposes. */ @@ -1003,7 +957,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati * param@[out] setCoinsRet Populated with the coins selected if successful. * param@[out] nValueRet Used to return the total value of selected coins. */ - bool SelectCoinsMinConf(const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, std::vector coins, std::set& setCoinsRet, CAmount& nValueRet, const CoinSelectionParams& coin_selection_params, bool& bnb_used, CoinType nCoinType = CoinType::ALL_COINS) const; + bool SelectCoinsMinConf(const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, std::vector coins, std::set& setCoinsRet, CAmount& nValueRet, const CoinSelectionParams& coin_selection_params, CoinType nCoinType = CoinType::ALL_COINS) const; // Coin selection bool SelectTxDSInsByDenomination(int nDenom, CAmount nValueMax, std::vector& vecTxDSInRet); @@ -1030,7 +984,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati bool IsSpentKey(const uint256& hash, unsigned int n) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void SetSpentKeyState(WalletBatch& batch, const uint256& hash, unsigned int n, bool used, std::set& tx_destinations) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); - std::vector GroupOutputs(const std::vector& outputs, bool separate_coins, const CFeeRate& effective_feerate, const CFeeRate& long_term_feerate, const CoinEligibilityFilter& filter, bool positive_only) const; + std::vector GroupOutputs(const std::vector& outputs, const CoinSelectionParams& coin_sel_params, const CoinEligibilityFilter& filter, bool positive_only) const; void RecalculateMixedCredit(const uint256 hash) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); From 959172874ad626acbd0aa920bacd28d903099aa0 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Wed, 10 Feb 2021 16:06:01 -0500 Subject: [PATCH 02/16] merge bitcoin#21207: CWallet transaction code out of wallet.cpp/.h --- src/Makefile.am | 6 + src/wallet/receive.cpp | 478 +++++++ src/wallet/receive.h | 20 + src/wallet/spend.cpp | 1043 +++++++++++++++ src/wallet/spend.h | 64 + src/wallet/transaction.cpp | 25 + src/wallet/transaction.h | 383 ++++++ src/wallet/wallet.cpp | 1632 +---------------------- src/wallet/wallet.h | 423 +----- test/lint/lint-circular-dependencies.py | 4 + 10 files changed, 2085 insertions(+), 1993 deletions(-) create mode 100644 src/wallet/receive.cpp create mode 100644 src/wallet/receive.h create mode 100644 src/wallet/spend.cpp create mode 100644 src/wallet/spend.h create mode 100644 src/wallet/transaction.cpp create mode 100644 src/wallet/transaction.h diff --git a/src/Makefile.am b/src/Makefile.am index 665102a2cadd5..090b8924fe419 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -401,10 +401,13 @@ BITCOIN_CORE_H = \ wallet/hdchain.h \ wallet/ismine.h \ wallet/load.h \ + wallet/receive.h \ wallet/rpcwallet.h \ wallet/salvage.h \ wallet/scriptpubkeyman.h \ + wallet/spend.h \ wallet/sqlite.h \ + wallet/transaction.h \ wallet/wallet.h \ wallet/walletdb.h \ wallet/wallettool.h \ @@ -596,9 +599,12 @@ libbitcoin_wallet_a_SOURCES = \ wallet/hdchain.cpp \ wallet/interfaces.cpp \ wallet/load.cpp \ + wallet/receive.cpp \ wallet/rpcdump.cpp \ wallet/rpcwallet.cpp \ wallet/scriptpubkeyman.cpp \ + wallet/spend.cpp \ + wallet/transaction.cpp \ wallet/wallet.cpp \ wallet/walletdb.cpp \ wallet/walletutil.cpp \ diff --git a/src/wallet/receive.cpp b/src/wallet/receive.cpp new file mode 100644 index 0000000000000..f23d83db1f1bd --- /dev/null +++ b/src/wallet/receive.cpp @@ -0,0 +1,478 @@ +// Copyright (c) 2021 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 + +isminetype CWallet::IsMine(const CTxIn &txin) const +{ + AssertLockHeld(cs_wallet); + std::map::const_iterator mi = mapWallet.find(txin.prevout.hash); + if (mi != mapWallet.end()) + { + const CWalletTx& prev = (*mi).second; + if (txin.prevout.n < prev.tx->vout.size()) + return IsMine(prev.tx->vout[txin.prevout.n]); + } + return ISMINE_NO; +} + +bool CWallet::IsAllFromMe(const CTransaction& tx, const isminefilter& filter) const +{ + LOCK(cs_wallet); + + for (const CTxIn& txin : tx.vin) + { + auto mi = mapWallet.find(txin.prevout.hash); + if (mi == mapWallet.end()) + return false; // any unknown inputs can't be from us + + const CWalletTx& prev = (*mi).second; + + if (txin.prevout.n >= prev.tx->vout.size()) + return false; // invalid input! + + if (!(IsMine(prev.tx->vout[txin.prevout.n]) & filter)) + return false; + } + return true; +} + +CAmount CWallet::GetCredit(const CTxOut& txout, const isminefilter& filter) const +{ + if (!MoneyRange(txout.nValue)) + throw std::runtime_error(std::string(__func__) + ": value out of range"); + LOCK(cs_wallet); + return ((IsMine(txout) & filter) ? txout.nValue : 0); +} + +CAmount CWallet::GetCredit(const CTransaction& tx, const isminefilter& filter) const +{ + CAmount nCredit = 0; + for (const CTxOut& txout : tx.vout) + { + nCredit += GetCredit(txout, filter); + if (!MoneyRange(nCredit)) + throw std::runtime_error(std::string(__func__) + ": value out of range"); + } + return nCredit; +} + +bool CWallet::IsChange(const CScript& script) const +{ + // TODO: fix handling of 'change' outputs. The assumption is that any + // payment to a script that is ours, but is not in the address book + // is change. That assumption is likely to break when we implement multisignature + // wallets that return change back into a multi-signature-protected address; + // a better way of identifying which outputs are 'the send' and which are + // 'the change' will need to be implemented (maybe extend CWalletTx to remember + // which output, if any, was change). + AssertLockHeld(cs_wallet); + if (IsMine(script)) + { + CTxDestination address; + if (!ExtractDestination(script, address)) + return true; + if (!FindAddressBookEntry(address)) { + return true; + } + } + return false; +} + +bool CWallet::IsChange(const CTxOut& txout) const +{ + return IsChange(txout.scriptPubKey); +} + +CAmount CWallet::GetChange(const CTxOut& txout) const +{ + AssertLockHeld(cs_wallet); + if (!MoneyRange(txout.nValue)) + throw std::runtime_error(std::string(__func__) + ": value out of range"); + return (IsChange(txout) ? txout.nValue : 0); +} + +CAmount CWallet::GetChange(const CTransaction& tx) const +{ + LOCK(cs_wallet); + CAmount nChange = 0; + for (const CTxOut& txout : tx.vout) + { + nChange += GetChange(txout); + if (!MoneyRange(nChange)) + throw std::runtime_error(std::string(__func__) + ": value out of range"); + } + return nChange; +} + +CAmount CWalletTx::GetCachableAmount(AmountType type, const isminefilter& filter, bool recalculate) const +{ + auto& amount = m_amounts[type]; + if (recalculate || !amount.m_cached[filter]) { + amount.Set(filter, type == DEBIT ? pwallet->GetDebit(*tx, filter) : pwallet->GetCredit(*tx, filter)); + m_is_cache_empty = false; + } + return amount.m_value[filter]; +} + +CAmount CWalletTx::GetCredit(const isminefilter& filter) const +{ + // Must wait until coinbase is safely deep enough in the chain before valuing it + if (IsImmatureCoinBase()) + return 0; + + CAmount credit = 0; + if (filter & ISMINE_SPENDABLE) { + // GetBalance can assume transactions in mapWallet won't change + credit += GetCachableAmount(CREDIT, ISMINE_SPENDABLE); + } + if (filter & ISMINE_WATCH_ONLY) { + credit += GetCachableAmount(CREDIT, ISMINE_WATCH_ONLY); + } + return credit; +} + +CAmount CWalletTx::GetDebit(const isminefilter& filter) const +{ + if (tx->vin.empty()) + return 0; + + CAmount debit = 0; + if (filter & ISMINE_SPENDABLE) { + debit += GetCachableAmount(DEBIT, ISMINE_SPENDABLE); + } + if (filter & ISMINE_WATCH_ONLY) { + debit += GetCachableAmount(DEBIT, ISMINE_WATCH_ONLY); + } + return debit; +} + +CAmount CWalletTx::GetChange() const +{ + if (fChangeCached) + return nChangeCached; + nChangeCached = pwallet->GetChange(*tx); + fChangeCached = true; + return nChangeCached; +} + +CAmount CWalletTx::GetImmatureCredit(bool fUseCache) const +{ + if (IsImmatureCoinBase() && IsInMainChain()) { + return GetCachableAmount(IMMATURE_CREDIT, ISMINE_SPENDABLE, !fUseCache); + } + + return 0; +} + +CAmount CWalletTx::GetImmatureWatchOnlyCredit(const bool fUseCache) const +{ + if (IsImmatureCoinBase() && IsInMainChain()) { + return GetCachableAmount(IMMATURE_CREDIT, ISMINE_WATCH_ONLY, !fUseCache); + } + + return 0; +} + +CAmount CWalletTx::GetAvailableCredit(bool fUseCache, const isminefilter& filter) const +{ + if (pwallet == nullptr) + return 0; + + // Avoid caching ismine for NO or ALL cases (could remove this check and simplify in the future). + bool allow_cache = (filter & ISMINE_ALL) && (filter & ISMINE_ALL) != ISMINE_ALL; + + // Must wait until coinbase is safely deep enough in the chain before valuing it + if (IsImmatureCoinBase()) + return 0; + + if (fUseCache && allow_cache && m_amounts[AVAILABLE_CREDIT].m_cached[filter]) { + return m_amounts[AVAILABLE_CREDIT].m_value[filter]; + } + + bool allow_used_addresses = (filter & ISMINE_USED) || !pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE); + CAmount nCredit = 0; + uint256 hashTx = GetHash(); + for (unsigned int i = 0; i < tx->vout.size(); i++) + { + if (!pwallet->IsSpent(hashTx, i) && (allow_used_addresses || !pwallet->IsSpentKey(hashTx, i))) { + const CTxOut &txout = tx->vout[i]; + nCredit += pwallet->GetCredit(txout, filter); + if (!MoneyRange(nCredit)) + throw std::runtime_error(std::string(__func__) + " : value out of range"); + } + } + + if (allow_cache) { + m_amounts[AVAILABLE_CREDIT].Set(filter, nCredit); + m_is_cache_empty = false; + } + + return nCredit; +} + +void CWalletTx::GetAmounts(std::list& listReceived, + std::list& listSent, CAmount& nFee, const isminefilter& filter) const +{ + nFee = 0; + listReceived.clear(); + listSent.clear(); + + // Compute fee: + CAmount nDebit = GetDebit(filter); + if (nDebit > 0) // debit>0 means we signed/sent this transaction + { + CAmount nValueOut = tx->GetValueOut(); + nFee = nDebit - nValueOut; + } + + LOCK(pwallet->cs_wallet); + // Sent/received. + for (unsigned int i = 0; i < tx->vout.size(); ++i) + { + const CTxOut& txout = tx->vout[i]; + isminetype fIsMine = pwallet->IsMine(txout); + // Only need to handle txouts if AT LEAST one of these is true: + // 1) they debit from us (sent) + // 2) the output is to us (received) + if (nDebit > 0) + { + // Don't report 'change' txouts + if (pwallet->IsChange(txout)) + continue; + } + else if (!(fIsMine & filter)) + continue; + + // In either case, we need to get the destination address + CTxDestination address; + + if (!ExtractDestination(txout.scriptPubKey, address) && !txout.scriptPubKey.IsUnspendable()) + { + pwallet->WalletLogPrintf("CWalletTx::GetAmounts: Unknown transaction type found, txid %s\n", + this->GetHash().ToString()); + address = CNoDestination(); + } + + COutputEntry output = {address, txout.nValue, (int)i}; + + // If we are debited by the transaction, add the output as a "sent" entry + if (nDebit > 0) + listSent.push_back(output); + + // If we are receiving the output, add it as a "received" entry + if (fIsMine & filter) + listReceived.push_back(output); + } + +} + +bool CWallet::IsTrusted(const CWalletTx& wtx, std::set& trusted_parents) const +{ + AssertLockHeld(cs_wallet); + // Quick answer in most cases + if (!chain().checkFinalTx(*wtx.tx)) return false; + int nDepth = wtx.GetDepthInMainChain(); + if (nDepth >= 1) return true; + if (nDepth < 0) return false; + if (wtx.IsLockedByInstantSend()) return true; + // using wtx's cached debit + if (!m_spend_zero_conf_change || !wtx.IsFromMe(ISMINE_ALL)) return false; + + // Don't trust unconfirmed transactions from us unless they are in the mempool. + if (!wtx.InMempool()) return false; + + // Trusted if all inputs are from us and are in the mempool: + for (const CTxIn& txin : wtx.tx->vin) + { + // Transactions not sent by us: not trusted + const CWalletTx* parent = GetWalletTx(txin.prevout.hash); + if (parent == nullptr) return false; + const CTxOut& parentOut = parent->tx->vout[txin.prevout.n]; + // Check that this specific input being spent is trusted + if (IsMine(parentOut) != ISMINE_SPENDABLE) return false; + // If we've already trusted this parent, continue + if (trusted_parents.count(parent->GetHash())) continue; + // Recurse to check that the parent is also trusted + if (!IsTrusted(*parent, trusted_parents)) return false; + trusted_parents.insert(parent->GetHash()); + } + return true; +} + +bool CWalletTx::IsTrusted() const +{ + std::set trusted_parents; + LOCK(pwallet->cs_wallet); + return pwallet->IsTrusted(*this, trusted_parents); +} + +CWallet::Balance CWallet::GetBalance(const int min_depth, const bool avoid_reuse, const bool fAddLocked, const CCoinControl* coinControl) const +{ + Balance ret; + isminefilter reuse_filter = avoid_reuse ? ISMINE_NO : ISMINE_USED; + { + LOCK(cs_wallet); + std::set trusted_parents; + for (const auto* pcoin : GetSpendableTXs()) { + const auto& wtx{*pcoin}; + + const bool is_trusted{IsTrusted(*&wtx, trusted_parents)}; + const int tx_depth{wtx.GetDepthInMainChain()}; + const CAmount tx_credit_mine{wtx.GetAvailableCredit(/* fUseCache */ true, ISMINE_SPENDABLE | reuse_filter)}; + const CAmount tx_credit_watchonly{wtx.GetAvailableCredit(/* fUseCache */ true, ISMINE_WATCH_ONLY | reuse_filter)}; + if (is_trusted && ((tx_depth >= min_depth) || (fAddLocked && wtx.IsLockedByInstantSend()))) { + ret.m_mine_trusted += tx_credit_mine; + ret.m_watchonly_trusted += tx_credit_watchonly; + } + if (!is_trusted && tx_depth == 0 && wtx.InMempool()) { + ret.m_mine_untrusted_pending += tx_credit_mine; + ret.m_watchonly_untrusted_pending += tx_credit_watchonly; + } + ret.m_mine_immature += wtx.GetImmatureCredit(); + ret.m_watchonly_immature += wtx.GetImmatureWatchOnlyCredit(); + if (CCoinJoinClientOptions::IsEnabled()) { + ret.m_anonymized += wtx.GetAnonymizedCredit(coinControl); + ret.m_denominated_trusted += wtx.GetDenominatedCredit(false); + ret.m_denominated_untrusted_pending += wtx.GetDenominatedCredit(true); + } + } + } + return ret; +} + +std::map CWallet::GetAddressBalances() const +{ + std::map balances; + + { + LOCK(cs_wallet); + std::set trusted_parents; + for (const auto& walletEntry : mapWallet) + { + const CWalletTx& wtx = walletEntry.second; + + if (!IsTrusted(*&wtx, trusted_parents)) + continue; + + if (wtx.IsImmatureCoinBase()) + continue; + + int nDepth = wtx.GetDepthInMainChain(); + if ((nDepth < (wtx.IsFromMe(ISMINE_ALL) ? 0 : 1)) && !wtx.IsLockedByInstantSend()) + continue; + + for (unsigned int i = 0; i < wtx.tx->vout.size(); i++) + { + CTxDestination addr; + if (!IsMine(wtx.tx->vout[i])) + continue; + if(!ExtractDestination(wtx.tx->vout[i].scriptPubKey, addr)) + continue; + + CAmount n = IsSpent(walletEntry.first, i) ? 0 : wtx.tx->vout[i].nValue; + balances[addr] += n; + } + } + } + + return balances; +} + +std::set< std::set > CWallet::GetAddressGroupings() const +{ + AssertLockHeld(cs_wallet); + std::set< std::set > groupings; + std::set grouping; + + for (const auto& walletEntry : mapWallet) + { + const CWalletTx& wtx = walletEntry.second; + + if (wtx.tx->vin.size() > 0) + { + bool any_mine = false; + // group all input addresses with each other + for (const CTxIn& txin : wtx.tx->vin) + { + CTxDestination address; + if(!IsMine(txin)) /* If this input isn't mine, ignore it */ + continue; + if(!ExtractDestination(mapWallet.at(txin.prevout.hash).tx->vout[txin.prevout.n].scriptPubKey, address)) + continue; + grouping.insert(address); + any_mine = true; + } + + // group change with input addresses + if (any_mine) + { + for (const CTxOut& txout : wtx.tx->vout) + if (IsChange(txout)) + { + CTxDestination txoutAddr; + if(!ExtractDestination(txout.scriptPubKey, txoutAddr)) + continue; + grouping.insert(txoutAddr); + } + } + if (grouping.size() > 0) + { + groupings.insert(grouping); + grouping.clear(); + } + } + + // group lone addrs by themselves + for (const auto& txout : wtx.tx->vout) + if (IsMine(txout)) + { + CTxDestination address; + if(!ExtractDestination(txout.scriptPubKey, address)) + continue; + grouping.insert(address); + groupings.insert(grouping); + grouping.clear(); + } + } + + std::set< std::set* > uniqueGroupings; // a set of pointers to groups of addresses + std::map< CTxDestination, std::set* > setmap; // map addresses to the unique group containing it + for (std::set _grouping : groupings) + { + // make a set of all the groups hit by this new group + std::set< std::set* > hits; + std::map< CTxDestination, std::set* >::iterator it; + for (const CTxDestination& address : _grouping) + if ((it = setmap.find(address)) != setmap.end()) + hits.insert((*it).second); + + // merge all hit groups into a new single group and delete old groups + std::set* merged = new std::set(_grouping); + for (std::set* hit : hits) + { + merged->insert(hit->begin(), hit->end()); + uniqueGroupings.erase(hit); + delete hit; + } + uniqueGroupings.insert(merged); + + // update setmap + for (const CTxDestination& element : *merged) + setmap[element] = merged; + } + + std::set< std::set > ret; + for (const std::set* uniqueGrouping : uniqueGroupings) + { + ret.insert(*uniqueGrouping); + delete uniqueGrouping; + } + + return ret; +} diff --git a/src/wallet/receive.h b/src/wallet/receive.h new file mode 100644 index 0000000000000..9396a93357cb0 --- /dev/null +++ b/src/wallet/receive.h @@ -0,0 +1,20 @@ +// Copyright (c) 2021 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_RECEIVE_H +#define BITCOIN_WALLET_RECEIVE_H + +#include +#include +#include +#include + +struct COutputEntry +{ + CTxDestination destination; + CAmount amount; + int vout; +}; + +#endif // BITCOIN_WALLET_RECEIVE_H diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp new file mode 100644 index 0000000000000..422f7e236ee28 --- /dev/null +++ b/src/wallet/spend.cpp @@ -0,0 +1,1043 @@ +// Copyright (c) 2021 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 + +using interfaces::FoundBlock; + +static constexpr size_t OUTPUT_GROUP_MAX_ENTRIES{100}; + +std::string COutput::ToString() const +{ + return strprintf("COutput(%s, %d, %d) [%s]", tx->GetHash().ToString(), i, nDepth, FormatMoney(tx->tx->vout[i].nValue)); +} + +int CalculateMaximumSignedInputSize(const CTxOut& txout, const CWallet* wallet, bool use_max_sig) +{ + CMutableTransaction txn; + txn.vin.push_back(CTxIn(COutPoint())); + if (!wallet->DummySignInput(txn.vin[0], txout, use_max_sig)) { + return -1; + } + return ::GetSerializeSize(txn.vin[0], PROTOCOL_VERSION); +} + +// txouts needs to be in the order of tx.vin +int64_t CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *wallet, const std::vector& txouts, bool use_max_sig) +{ + CMutableTransaction txNew(tx); + if (!wallet->DummySignTx(txNew, txouts, use_max_sig)) { + return -1; + } + return ::GetSerializeSize(txNew, PROTOCOL_VERSION); +} + +int64_t CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *wallet, bool use_max_sig) +{ + std::vector txouts; + for (const CTxIn& input : tx.vin) { + const auto mi = wallet->mapWallet.find(input.prevout.hash); + // Can not estimate size without knowing the input details + if (mi == wallet->mapWallet.end()) { + return -1; + } + assert(input.prevout.n < mi->second.tx->vout.size()); + txouts.emplace_back(mi->second.tx->vout[input.prevout.n]); + } + return CalculateMaximumSignedTxSize(tx, wallet, txouts, use_max_sig); +} + +void CWallet::AvailableCoins(std::vector &vCoins, const CCoinControl* coinControl, const CAmount& nMinimumAmount, const CAmount& nMaximumAmount, const CAmount &nMinimumSumAmount, const uint64_t nMaximumCount) const +{ + AssertLockHeld(cs_wallet); + + vCoins.clear(); + CoinType nCoinType = coinControl ? coinControl->nCoinType : CoinType::ALL_COINS; + + CAmount nTotal = 0; + // Either the WALLET_FLAG_AVOID_REUSE flag is not set (in which case we always allow), or we default to avoiding, and only in the case where + // a coin control object is provided, and has the avoid address reuse flag set to false, do we allow already used addresses + bool allow_used_addresses = !IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE) || (coinControl && !coinControl->m_avoid_address_reuse); + const int min_depth = {coinControl ? coinControl->m_min_depth : DEFAULT_MIN_DEPTH}; + const int max_depth = {coinControl ? coinControl->m_max_depth : DEFAULT_MAX_DEPTH}; + const bool only_safe = {coinControl ? !coinControl->m_include_unsafe_inputs : true}; + + std::set trusted_parents; + for (const auto* pcoin : GetSpendableTXs()) { + const auto& wtx{*pcoin}; + + const uint256& wtxid = wtx.GetHash(); + + if (!chain().checkFinalTx(*wtx.tx)) + continue; + + if (wtx.IsImmatureCoinBase()) + continue; + + int nDepth = wtx.GetDepthInMainChain(); + + // We should not consider coins which aren't at least in our mempool + // It's possible for these to be conflicted via ancestors which we may never be able to detect + if (nDepth == 0 && !wtx.InMempool()) + continue; + + bool safeTx = IsTrusted(*&wtx, trusted_parents); + + if (only_safe && !safeTx) { + continue; + } + + if (nDepth < min_depth || nDepth > max_depth) { + continue; + } + + for (unsigned int i = 0; i < wtx.tx->vout.size(); i++) { + bool found = false; + switch (nCoinType) { + case CoinType::ONLY_FULLY_MIXED: { + found = CoinJoin::IsDenominatedAmount(wtx.tx->vout[i].nValue) && + IsFullyMixed(COutPoint(wtxid, i)); + break; + } + case CoinType::ONLY_READY_TO_MIX: { + found = CoinJoin::IsDenominatedAmount(wtx.tx->vout[i].nValue) && + !IsFullyMixed(COutPoint(wtxid, i)); + break; + } + case CoinType::ONLY_NONDENOMINATED: { + // NOTE: do not use collateral amounts + found = !CoinJoin::IsCollateralAmount(wtx.tx->vout[i].nValue) && + !CoinJoin::IsDenominatedAmount(wtx.tx->vout[i].nValue); + break; + } + case CoinType::ONLY_MASTERNODE_COLLATERAL: { + found = dmn_types::IsCollateralAmount(wtx.tx->vout[i].nValue); + break; + } + case CoinType::ONLY_COINJOIN_COLLATERAL: { + found = CoinJoin::IsCollateralAmount(wtx.tx->vout[i].nValue); + break; + } + case CoinType::ALL_COINS: { + found = true; + break; + } + } // no default case, so the compiler can warn about missing cases + if(!found) continue; + + // Only consider selected coins if add_inputs is false + if (coinControl && !coinControl->m_add_inputs && !coinControl->IsSelected(COutPoint(wtxid, i))) { + continue; + } + + if (wtx.tx->vout[i].nValue < nMinimumAmount || wtx.tx->vout[i].nValue > nMaximumAmount) + continue; + + if (coinControl && coinControl->HasSelected() && !coinControl->fAllowOtherInputs && !coinControl->IsSelected(COutPoint(wtxid, i))) + continue; + + if (IsLockedCoin(wtxid, i) && nCoinType != CoinType::ONLY_MASTERNODE_COLLATERAL) + continue; + + if (IsSpent(wtxid, i)) + continue; + + isminetype mine = IsMine(wtx.tx->vout[i]); + + if (mine == ISMINE_NO) { + continue; + } + + if (!allow_used_addresses && IsSpentKey(wtxid, i)) { + continue; + } + + std::unique_ptr provider = GetSolvingProvider(wtx.tx->vout[i].scriptPubKey); + + bool solvable = provider ? IsSolvable(*provider, wtx.tx->vout[i].scriptPubKey) : false; + bool spendable = ((mine & ISMINE_SPENDABLE) != ISMINE_NO) || (((mine & ISMINE_WATCH_ONLY) != ISMINE_NO) && (coinControl && coinControl->fAllowWatchOnly && solvable)); + + vCoins.push_back(COutput(&wtx, i, nDepth, spendable, solvable, safeTx, (coinControl && coinControl->fAllowWatchOnly))); + + // Checks the sum amount of all UTXO's. + if (nMinimumSumAmount != MAX_MONEY) { + nTotal += wtx.tx->vout[i].nValue; + + if (nTotal >= nMinimumSumAmount) { + return; + } + } + + // Checks the maximum number of UTXO's. + if (nMaximumCount > 0 && vCoins.size() >= nMaximumCount) { + return; + } + } + } +} + +CAmount CWallet::GetAvailableBalance(const CCoinControl* coinControl) const +{ + LOCK(cs_wallet); + + CAmount balance = 0; + std::vector vCoins; + AvailableCoins(vCoins, coinControl); + for (const COutput& out : vCoins) { + if (out.fSpendable) { + balance += out.tx->tx->vout[out.i].nValue; + } + } + return balance; +} + +const CTxOut& CWallet::FindNonChangeParentOutput(const CTransaction& tx, int output) const +{ + AssertLockHeld(cs_wallet); + const CTransaction* ptx = &tx; + int n = output; + while (IsChange(ptx->vout[n]) && ptx->vin.size() > 0) { + const COutPoint& prevout = ptx->vin[0].prevout; + auto it = mapWallet.find(prevout.hash); + if (it == mapWallet.end() || it->second.tx->vout.size() <= prevout.n || + !IsMine(it->second.tx->vout[prevout.n])) { + break; + } + ptx = it->second.tx.get(); + n = prevout.n; + } + return ptx->vout[n]; +} + +std::map> CWallet::ListCoins() const +{ + AssertLockHeld(cs_wallet); + + std::map> result; + std::vector availableCoins; + + AvailableCoins(availableCoins); + + for (const COutput& coin : availableCoins) { + CTxDestination address; + if ((coin.fSpendable || (IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && coin.fSolvable)) && + ExtractDestination(FindNonChangeParentOutput(*coin.tx->tx, coin.i).scriptPubKey, address)) { + result[address].emplace_back(std::move(coin)); + } + } + + std::vector lockedCoins; + ListLockedCoins(lockedCoins); + // Include watch-only for LegacyScriptPubKeyMan wallets without private keys + const bool include_watch_only = GetLegacyScriptPubKeyMan() && IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS); + const isminetype is_mine_filter = include_watch_only ? ISMINE_WATCH_ONLY : ISMINE_SPENDABLE; + for (const COutPoint& output : lockedCoins) { + auto it = mapWallet.find(output.hash); + if (it != mapWallet.end()) { + int depth = it->second.GetDepthInMainChain(); + if (depth >= 0 && output.n < it->second.tx->vout.size() && + IsMine(it->second.tx->vout[output.n]) == is_mine_filter + ) { + CTxDestination address; + if (ExtractDestination(FindNonChangeParentOutput(*it->second.tx, output.n).scriptPubKey, address)) { + result[address].emplace_back( + &it->second, output.n, depth, true /* spendable */, true /* solvable */, false /* safe */); + } + } + } + } + + return result; +} + +static bool isGroupISLocked(const OutputGroup& group, interfaces::Chain& chain) +{ + return std::all_of(group.m_outputs.begin(), group.m_outputs.end(), [&chain](const auto& output) { + return chain.isInstantSendLockedTx(output.outpoint.hash); + }); +} + +std::vector CWallet::GroupOutputs(const std::vector& outputs, const CoinSelectionParams& coin_sel_params, const CoinEligibilityFilter& filter, bool positive_only) const +{ + std::vector groups_out; + + if (!coin_sel_params.m_avoid_partial_spends) { + // Allowing partial spends means no grouping. Each COutput gets its own OutputGroup. + for (const COutput& output : outputs) { + // Skip outputs we cannot spend + if (!output.fSpendable) continue; + + size_t ancestors, descendants; + chain().getTransactionAncestry(output.tx->GetHash(), ancestors, descendants); + CInputCoin input_coin = output.GetInputCoin(); + + // Make an OutputGroup containing just this output + OutputGroup group{coin_sel_params}; + group.Insert(input_coin, output.nDepth, output.tx->IsFromMe(ISMINE_ALL), ancestors, descendants, positive_only); + + // Check the OutputGroup's eligibility. Only add the eligible ones. + if (positive_only && group.GetSelectionAmount() <= 0) continue; + bool isISLocked = isGroupISLocked(group, chain()); + if (group.m_outputs.size() > 0 && group.EligibleForSpending(filter, isISLocked)) groups_out.push_back(group); + } + return groups_out; + } + + // We want to combine COutputs that have the same scriptPubKey into single OutputGroups + // except when there are more than OUTPUT_GROUP_MAX_ENTRIES COutputs grouped in an OutputGroup. + // To do this, we maintain a map where the key is the scriptPubKey and the value is a vector of OutputGroups. + // For each COutput, we check if the scriptPubKey is in the map, and if it is, the COutput's CInputCoin is added + // to the last OutputGroup in the vector for the scriptPubKey. When the last OutputGroup has + // OUTPUT_GROUP_MAX_ENTRIES CInputCoins, a new OutputGroup is added to the end of the vector. + std::map> spk_to_groups_map; + for (const auto& output : outputs) { + // Skip outputs we cannot spend + if (!output.fSpendable) continue; + + size_t ancestors, descendants; + chain().getTransactionAncestry(output.tx->GetHash(), ancestors, descendants); + CInputCoin input_coin = output.GetInputCoin(); + CScript spk = input_coin.txout.scriptPubKey; + + std::vector& groups = spk_to_groups_map[spk]; + + if (groups.size() == 0) { + // No OutputGroups for this scriptPubKey yet, add one + groups.emplace_back(coin_sel_params); + } + + // Get the last OutputGroup in the vector so that we can add the CInputCoin to it + // A pointer is used here so that group can be reassigned later if it is full. + OutputGroup* group = &groups.back(); + + // Check if this OutputGroup is full. We limit to OUTPUT_GROUP_MAX_ENTRIES when using -avoidpartialspends + // to avoid surprising users with very high fees. + if (group->m_outputs.size() >= OUTPUT_GROUP_MAX_ENTRIES) { + // The last output group is full, add a new group to the vector and use that group for the insertion + groups.emplace_back(coin_sel_params); + group = &groups.back(); + } + + // Add the input_coin to group + group->Insert(input_coin, output.nDepth, output.tx->IsFromMe(ISMINE_ALL), ancestors, descendants, positive_only); + } + + // Now we go through the entire map and pull out the OutputGroups + for (const auto& spk_and_groups_pair: spk_to_groups_map) { + const std::vector& groups_per_spk= spk_and_groups_pair.second; + + // Go through the vector backwards. This allows for the first item we deal with being the partial group. + for (auto group_it = groups_per_spk.rbegin(); group_it != groups_per_spk.rend(); group_it++) { + const OutputGroup& group = *group_it; + + // Don't include partial groups if there are full groups too and we don't want partial groups + if (group_it == groups_per_spk.rbegin() && groups_per_spk.size() > 1 && !filter.m_include_partial_groups) { + continue; + } + + // Check the OutputGroup's eligibility. Only add the eligible ones. + if (positive_only && group.GetSelectionAmount() <= 0) continue; + bool isISLocked = isGroupISLocked(group, chain()); + if (group.m_outputs.size() > 0 && group.EligibleForSpending(filter, isISLocked)) groups_out.push_back(group); + } + } + + return groups_out; +} + +bool CWallet::SelectCoinsMinConf(const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, std::vector coins, + std::set& setCoinsRet, CAmount& nValueRet, const CoinSelectionParams& coin_selection_params, CoinType nCoinType) const +{ + setCoinsRet.clear(); + nValueRet = 0; + + // Note that unlike KnapsackSolver, we do not include the fee for creating a change output as BnB will not create a change output. + std::vector positive_groups = GroupOutputs(coins, coin_selection_params, eligibility_filter, true /* positive_only */); + if (SelectCoinsBnB(positive_groups, nTargetValue, coin_selection_params.m_cost_of_change, setCoinsRet, nValueRet)) { + return true; + } + // The knapsack solver has some legacy behavior where it will spend dust outputs. We retain this behavior, so don't filter for positive only here. + std::vector all_groups = GroupOutputs(coins, coin_selection_params, eligibility_filter, false /* positive_only */); + // While nTargetValue includes the transaction fees for non-input things, it does not include the fee for creating a change output. + // So we need to include that for KnapsackSolver as well, as we are expecting to create a change output. + return KnapsackSolver(nTargetValue + coin_selection_params.m_change_fee, all_groups, setCoinsRet, nValueRet, nCoinType == CoinType::ONLY_FULLY_MIXED, m_default_max_tx_fee); +} + +bool CWallet::SelectCoins(const std::vector& vAvailableCoins, const CAmount& nTargetValue, std::set& setCoinsRet, CAmount& nValueRet, const CCoinControl& coin_control, CoinSelectionParams& coin_selection_params) const +{ + // Note: this function should never be used for "always free" tx types like dstx + + std::vector vCoins(vAvailableCoins); + CoinType nCoinType = coin_control.nCoinType; + CAmount value_to_select = nTargetValue; + + // coin control -> return all selected outputs (we want all selected to go into the transaction for sure) + if (coin_control.HasSelected() && !coin_control.fAllowOtherInputs) + { + for (const COutput& out : vCoins) + { + if(!out.fSpendable) + continue; + + nValueRet += out.tx->tx->vout[out.i].nValue; + setCoinsRet.insert(out.GetInputCoin()); + + if (!coin_control.fRequireAllInputs && nValueRet >= nTargetValue) { + // stop when we added at least one input and enough inputs to have at least nTargetValue funds + return true; + } + } + + return (nValueRet >= nTargetValue); + } + + // calculate value from preset inputs and store them + std::set setPresetCoins; + CAmount nValueFromPresetInputs = 0; + + std::vector vPresetInputs; + coin_control.ListSelected(vPresetInputs); + for (const COutPoint& outpoint : vPresetInputs) + { + std::map::const_iterator it = mapWallet.find(outpoint.hash); + if (it != mapWallet.end()) + { + const CWalletTx& wtx = it->second; + // Clearly invalid input, fail + if (wtx.tx->vout.size() <= outpoint.n) { + return false; + } + if (nCoinType == CoinType::ONLY_FULLY_MIXED) { + // Make sure to include mixed preset inputs only, + // even if some non-mixed inputs were manually selected via CoinControl + if (!IsFullyMixed(outpoint)) continue; + } + // Just to calculate the marginal byte size + CInputCoin coin(wtx.tx, outpoint.n, wtx.GetSpendSize(outpoint.n, false)); + nValueFromPresetInputs += coin.txout.nValue; + if (coin.m_input_bytes <= 0) { + return false; // Not solvable, can't estimate size for fee + } + coin.effective_value = coin.txout.nValue - coin_selection_params.m_effective_feerate.GetFee(coin.m_input_bytes); + if (coin_selection_params.m_subtract_fee_outputs) { + value_to_select -= coin.txout.nValue; + } else { + value_to_select -= coin.effective_value; + } + setPresetCoins.insert(coin); + } else { + return false; // TODO: Allow non-wallet inputs + } + } + + // remove preset inputs from vCoins so that Coin Selection doesn't pick them. + for (std::vector::iterator it = vCoins.begin(); it != vCoins.end() && coin_control.HasSelected();) + { + if (setPresetCoins.count(it->GetInputCoin())) + it = vCoins.erase(it); + else + ++it; + } + + unsigned int limit_ancestor_count = 0; + unsigned int limit_descendant_count = 0; + chain().getPackageLimits(limit_ancestor_count, limit_descendant_count); + const size_t max_ancestors = (size_t)std::max(1, limit_ancestor_count); + const size_t max_descendants = (size_t)std::max(1, limit_descendant_count); + const bool fRejectLongChains = gArgs.GetBoolArg("-walletrejectlongchains", DEFAULT_WALLET_REJECT_LONG_CHAINS); + + // form groups from remaining coins; note that preset coins will not + // automatically have their associated (same address) coins included + if (coin_control.m_avoid_partial_spends && vCoins.size() > OUTPUT_GROUP_MAX_ENTRIES) { + // Cases where we have 101+ outputs all pointing to the same destination may result in + // privacy leaks as they will potentially be deterministically sorted. We solve that by + // explicitly shuffling the outputs before processing + Shuffle(vCoins.begin(), vCoins.end(), FastRandomContext()); + } + // Coin Selection attempts to select inputs from a pool of eligible UTXOs to fund the + // transaction at a target feerate. If an attempt fails, more attempts may be made using a more + // permissive CoinEligibilityFilter. + const bool res = [&] { + // Pre-selected inputs already cover the target amount. + if (value_to_select <= 0) return true; + + // If possible, fund the transaction with confirmed UTXOs only. Prefer at least six + // confirmations on outputs received from other wallets and only spend confirmed change. + if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(1, 6, 0), vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) return true; + if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(1, 1, 0), vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) return true; + + // Fall back to using zero confirmation change (but with as few ancestors in the mempool as + // possible) if we cannot fund the transaction otherwise. + if (m_spend_zero_conf_change) { + if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, 2), vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) return true; + if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, std::min((size_t)4, max_ancestors/3), std::min((size_t)4, max_descendants/3)), + vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) { + return true; + } + if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, max_ancestors/2, max_descendants/2), + vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) { + return true; + } + // If partial groups are allowed, relax the requirement of spending OutputGroups (groups + // of UTXOs sent to the same address, which are obviously controlled by a single wallet) + // in their entirety. + if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, max_ancestors-1, max_descendants-1, true /* include_partial_groups */), + vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) { + return true; + } + // Try with unsafe inputs if they are allowed. This may spend unconfirmed outputs + // received from other wallets. + if (coin_control.m_include_unsafe_inputs + && SelectCoinsMinConf(value_to_select, + CoinEligibilityFilter(0 /* conf_mine */, 0 /* conf_theirs */, max_ancestors-1, max_descendants-1, true /* include_partial_groups */), + vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) { + return true; + } + // Try with unlimited ancestors/descendants. The transaction will still need to meet + // mempool ancestor/descendant policy to be accepted to mempool and broadcasted, but + // OutputGroups use heuristics that may overestimate ancestor/descendant counts. + if (!fRejectLongChains && SelectCoinsMinConf(value_to_select, + CoinEligibilityFilter(0, 1, std::numeric_limits::max(), std::numeric_limits::max(), true /* include_partial_groups */), + vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) { + return true; + } + } + // Coin Selection failed. + return false; + }(); + + // SelectCoinsMinConf clears setCoinsRet, so add the preset inputs from coin_control to the coinset + util::insert(setCoinsRet, setPresetCoins); + + // add preset inputs to the total value selected + nValueRet += nValueFromPresetInputs; + + return res; +} + +static bool IsCurrentForAntiFeeSniping(interfaces::Chain& chain, const uint256& block_hash) +{ + if (chain.isInitialBlockDownload()) { + return false; + } + constexpr int64_t MAX_ANTI_FEE_SNIPING_TIP_AGE = 8 * 60 * 60; // in seconds + int64_t block_time; + CHECK_NONFATAL(chain.findBlock(block_hash, FoundBlock().time(block_time))); + if (block_time < (GetTime() - MAX_ANTI_FEE_SNIPING_TIP_AGE)) { + return false; + } + return true; +} + +/** + * Return a height-based locktime for new transactions (uses the height of the + * current chain tip unless we are not synced with the current chain + */ +static uint32_t GetLocktimeForNewTransaction(interfaces::Chain& chain, const uint256& block_hash, int block_height) +{ + uint32_t locktime; + // Discourage fee sniping. + // + // For a large miner the value of the transactions in the best block and + // the mempool can exceed the cost of deliberately attempting to mine two + // blocks to orphan the current best block. By setting nLockTime such that + // only the next block can include the transaction, we discourage this + // practice as the height restricted and limited blocksize gives miners + // considering fee sniping fewer options for pulling off this attack. + // + // A simple way to think about this is from the wallet's point of view we + // always want the blockchain to move forward. By setting nLockTime this + // way we're basically making the statement that we only want this + // transaction to appear in the next block; we don't want to potentially + // encourage reorgs by allowing transactions to appear at lower heights + // than the next block in forks of the best chain. + // + // Of course, the subsidy is high enough, and transaction volume low + // enough, that fee sniping isn't a problem yet, but by implementing a fix + // now we ensure code won't be written that makes assumptions about + // nLockTime that preclude a fix later. + if (IsCurrentForAntiFeeSniping(chain, block_hash)) { + locktime = block_height; + + // Secondly occasionally randomly pick a nLockTime even further back, so + // that transactions that are delayed after signing for whatever reason, + // e.g. high-latency mix networks and some CoinJoin implementations, have + // better privacy. + if (GetRandInt(10) == 0) + locktime = std::max(0, (int)locktime - GetRandInt(100)); + } else { + // If our chain is lagging behind, we can't discourage fee sniping nor help + // the privacy of high-latency transactions. To avoid leaking a potentially + // unique "nLockTime fingerprint", set nLockTime to a constant. + locktime = 0; + } + assert(locktime < LOCKTIME_THRESHOLD); + return locktime; +} + +bool CWallet::CreateTransactionInternal( + const std::vector& vecSend, + CTransactionRef& tx, + CAmount& nFeeRet, + int& nChangePosInOut, + bilingual_str& error, + const CCoinControl& coin_control, + FeeCalculation& fee_calc_out, + bool sign, + int nExtraPayloadSize) +{ + CAmount nValue = 0; + ReserveDestination reservedest(this); + const bool sort_bip69{nChangePosInOut == -1}; + unsigned int nSubtractFeeFromAmount = 0; + for (const auto& recipient : vecSend) + { + if (nValue < 0 || recipient.nAmount < 0) + { + error = _("Transaction amounts must not be negative"); + return false; + } + nValue += recipient.nAmount; + + if (recipient.fSubtractFeeFromAmount) + nSubtractFeeFromAmount++; + } + if (vecSend.empty()) + { + error = _("Transaction must have at least one recipient"); + return false; + } + + CMutableTransaction txNew; + FeeCalculation feeCalc; + int nBytes{0}; + CAmount fee_needed{0}; + { + std::set setCoins; + LOCK(cs_wallet); + txNew.nLockTime = GetLocktimeForNewTransaction(chain(), GetLastBlockHash(), GetLastBlockHeight()); + { + std::vector vAvailableCoins; + AvailableCoins(vAvailableCoins, &coin_control, 1, MAX_MONEY, MAX_MONEY, 0); + CoinSelectionParams coin_selection_params; // Parameters for coin selection, init with dummy + coin_selection_params.m_avoid_partial_spends = coin_control.m_avoid_partial_spends; + + // Create change script that will be used if we need change + // TODO: pass in scriptChange instead of reservedest so + // change transaction isn't always pay-to-bitcoin-address + CScript scriptChange; + + // coin control: send change to custom address + if (!std::get_if(&coin_control.destChange)) { + scriptChange = GetScriptForDestination(coin_control.destChange); + } else { // no coin control: send change to newly generated address + // Note: We use a new key here to keep it from being obvious which side is the change. + // The drawback is that by not reusing a previous key, the change may be lost if a + // backup is restored, if the backup doesn't have the new private key for the change. + // If we reused the old key, it would be possible to add code to look for and + // rediscover unknown transactions that were written with keys of ours to recover + // post-backup change. + + // Reserve a new key pair from key pool. If it fails, provide a dummy + // destination in case we don't need change. + CTxDestination dest; + if (!reservedest.GetReservedDestination(dest, true)) { + error = _("Transaction needs a change address, but we can't generate it. Please call keypoolrefill first."); + } + scriptChange = GetScriptForDestination(dest); + // A valid destination implies a change script (and + // vice-versa). An empty change script will abort later, if the + // change keypool ran out, but change is required. + CHECK_NONFATAL(IsValidDestination(dest) != scriptChange.empty()); + } + CTxOut change_prototype_txout(0, scriptChange); + coin_selection_params.change_output_size = GetSerializeSize(change_prototype_txout); + + // Get size of spending the change output + int change_spend_size = CalculateMaximumSignedInputSize(change_prototype_txout, this); + // If the wallet doesn't know how to sign change output, assume p2sh-p2pkh + // as lower-bound to allow BnB to do it's thing + if (change_spend_size == -1) { + coin_selection_params.change_spend_size = DUMMY_NESTED_P2PKH_INPUT_SIZE; + } else { + coin_selection_params.change_spend_size = (size_t)change_spend_size; + } + + // Set discard feerate + coin_selection_params.m_discard_feerate = coin_control.m_discard_feerate ? *coin_control.m_discard_feerate : GetDiscardRate(*this); + + // Get the fee rate to use effective values in coin selection + coin_selection_params.m_effective_feerate = GetMinimumFeeRate(*this, coin_control, &feeCalc); + // Do not, ever, assume that it's fine to change the fee rate if the user has explicitly + // provided one + if (coin_control.m_feerate && coin_selection_params.m_effective_feerate > *coin_control.m_feerate) { + error = strprintf(_("Fee rate (%s) is lower than the minimum fee rate setting (%s)"), coin_control.m_feerate->ToString(FeeEstimateMode::DUFF_B), coin_selection_params.m_effective_feerate.ToString(FeeEstimateMode::DUFF_B)); + return false; + } + if (feeCalc.reason == FeeReason::FALLBACK && !m_allow_fallback_fee) { + // eventually allow a fallback fee + error = _("Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable -fallbackfee."); + return false; + } + + // Get long term estimate + CCoinControl cc_temp; + cc_temp.m_confirm_target = chain().estimateMaxBlocks(); + coin_selection_params.m_long_term_feerate = GetMinimumFeeRate(*this, cc_temp, nullptr); + + // Calculate the cost of change + // Cost of change is the cost of creating the change output + cost of spending the change output in the future. + // For creating the change output now, we use the effective feerate. + // For spending the change output in the future, we use the discard feerate for now. + // So cost of change = (change output size * effective feerate) + (size of spending change output * discard feerate) + coin_selection_params.m_change_fee = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.change_output_size); + coin_selection_params.m_cost_of_change = coin_selection_params.m_discard_feerate.GetFee(coin_selection_params.change_spend_size) + coin_selection_params.m_change_fee; + + coin_selection_params.m_subtract_fee_outputs = nSubtractFeeFromAmount != 0; // If we are doing subtract fee from recipient, don't use effective values + + // vouts to the payees + if (!coin_selection_params.m_subtract_fee_outputs) { + coin_selection_params.tx_noinputs_size = 9; // Static vsize overhead + outputs vsize. 4 nVersion, 4 nLocktime, 1 input count + coin_selection_params.tx_noinputs_size += GetSizeOfCompactSize(vecSend.size()); // bytes for output count + } + for (const auto& recipient : vecSend) + { + CTxOut txout(recipient.nAmount, recipient.scriptPubKey); + + // Include the fee cost for outputs. + if (!coin_selection_params.m_subtract_fee_outputs) { + coin_selection_params.tx_noinputs_size += ::GetSerializeSize(txout, PROTOCOL_VERSION); + } + + if (IsDust(txout, chain().relayDustFee())) + { + error = _("Transaction amount too small"); + return false; + } + txNew.vout.push_back(txout); + } + + // Include the fees for things that aren't inputs, excluding the change output + const CAmount not_input_fees = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.tx_noinputs_size); + CAmount nValueToSelect = nValue + not_input_fees; + + // Choose coins to use + CAmount inputs_sum = 0; + setCoins.clear(); + if (!SelectCoins(vAvailableCoins, /* nTargetValue */ nValueToSelect, setCoins, inputs_sum, coin_control, coin_selection_params)) { + if (coin_control.nCoinType == CoinType::ONLY_NONDENOMINATED) { + error = _("Unable to locate enough non-denominated funds for this transaction."); + } else if (coin_control.nCoinType == CoinType::ONLY_FULLY_MIXED) { + error = _("Unable to locate enough mixed funds for this transaction."); + error = error + Untranslated(" ") + strprintf(_("%s uses exact denominated amounts to send funds, you might simply need to mix some more coins."), gCoinJoinName); + } else { + error = _("Insufficient funds."); + } + return false; + } + + // Always make a change output + // We will reduce the fee from this change output later, and remove the output if it is too small. + const CAmount change_and_fee = inputs_sum - nValue; + assert(change_and_fee >= 0); + CTxOut newTxOut(change_and_fee, scriptChange); + + if (nChangePosInOut == -1) + { + // Insert change txn at random position: + nChangePosInOut = GetRandInt(txNew.vout.size()+1); + } + else if ((unsigned int)nChangePosInOut > txNew.vout.size()) + { + error = _("Transaction change output index out of range"); + return false; + } + + assert(nChangePosInOut != -1); + auto change_position = txNew.vout.insert(txNew.vout.begin() + nChangePosInOut, newTxOut); + + // We're making a copy of vecSend because it's const, sortedVecSend should be used + // in place of vecSend in all subsequent usage. + std::vector sortedVecSend{vecSend}; + if (sort_bip69) { + std::sort(txNew.vout.begin(), txNew.vout.end(), CompareOutputBIP69()); + // The output reduction loop uses vecSend to map to txNew.vout, we need to + // shuffle them both to ensure this mapping remains consistent + std::sort(sortedVecSend.begin(), sortedVecSend.end(), + [](const CRecipient& a, const CRecipient& b) { + return a.nAmount < b.nAmount || (a.nAmount == b.nAmount && a.scriptPubKey < b.scriptPubKey); + }); + + // If there was a change output added before, we must update its position now + if (const auto it = std::find(txNew.vout.begin(), txNew.vout.end(), newTxOut); it != txNew.vout.end()) { + change_position = it; + nChangePosInOut = std::distance(txNew.vout.begin(), change_position); + } + }; + + // Dummy fill vin for maximum size estimation + // + for (const auto& coin : setCoins) { + txNew.vin.push_back(CTxIn(coin.outpoint, CScript())); + } + + // Calculate the transaction fee + nBytes = CalculateMaximumSignedTxSize(CTransaction(txNew), this, coin_control.fAllowWatchOnly); + if (nBytes < 0) { + error = _("Signing transaction failed"); + return false; + } + + if (nExtraPayloadSize != 0) { + // account for extra payload in fee calculation + nBytes += GetSizeOfCompactSize(nExtraPayloadSize) + nExtraPayloadSize; + } + + fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes); + + if (nSubtractFeeFromAmount == 0) { + change_position->nValue -= fee_needed; + } + + // We want to drop the change to fees if: + // 1. The change output would be dust + // 2. The change is within the (almost) exact match window, i.e. it is less than or equal to the cost of the change output (cost_of_change) + // 3. We are working with fully mixed CoinJoin denominations + CAmount change_amount = change_position->nValue; + if (IsDust(*change_position, coin_selection_params.m_discard_feerate) || change_amount <= coin_selection_params.m_cost_of_change || coin_control.nCoinType == CoinType::ONLY_FULLY_MIXED) + { + nChangePosInOut = -1; + change_amount = 0; + txNew.vout.erase(change_position); + + nBytes = CalculateMaximumSignedTxSize(CTransaction(txNew), this, coin_control.fAllowWatchOnly); + fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes); + } + + nFeeRet = inputs_sum - nValue - change_amount; + + // Update nFeeRet in case fee_needed changed due to dropping the change output + if (fee_needed <= change_and_fee - change_amount) { + nFeeRet = change_and_fee - change_amount; + } + + // Reduce output values for subtractFeeFromAmount + if (nSubtractFeeFromAmount != 0) { + CAmount to_reduce = fee_needed + change_amount - change_and_fee; + int i = 0; + bool fFirst = true; + for (const auto& recipient : sortedVecSend) + { + if (i == nChangePosInOut) { + ++i; + } + CTxOut& txout = txNew.vout[i]; + + if (recipient.fSubtractFeeFromAmount) + { + txout.nValue -= to_reduce / nSubtractFeeFromAmount; // Subtract fee equally from each selected recipient + + if (fFirst) // first receiver pays the remainder not divisible by output count + { + fFirst = false; + txout.nValue -= to_reduce % nSubtractFeeFromAmount; + } + // Error if this output is reduced to be below dust + if (IsDust(txout, chain().relayDustFee())) { + if (txout.nValue < 0) { + error = _("The transaction amount is too small to pay the fee"); + } else { + error = _("The transaction amount is too small to send after the fee has been deducted"); + } + return false; + } + } + ++i; + } + nFeeRet = fee_needed; + } + + // Give up if change keypool ran out and change is required + if (scriptChange.empty() && nChangePosInOut != -1) { + return false; + } + } + + // Fill in final vin and shuffle/sort it + txNew.vin.clear(); + + // Note how the sequence number is set to non-maxint so that + // the nLockTime set above actually works. + const uint32_t nSequence = CTxIn::SEQUENCE_FINAL - 1; + for (const auto& coin : setCoins) { + txNew.vin.push_back(CTxIn(coin.outpoint, CScript(), nSequence)); + } + if (sort_bip69) { std::sort(txNew.vin.begin(), txNew.vin.end(), CompareInputBIP69()); } + else { Shuffle(txNew.vin.begin(), txNew.vin.end(), FastRandomContext()); } + + if (sign && !SignTransaction(txNew)) { + error = _("Signing transaction failed"); + return false; + } + + // Return the constructed transaction data. + tx = MakeTransactionRef(std::move(txNew)); + + // Limit size + if ((sign && ::GetSerializeSize(*tx, PROTOCOL_VERSION) > MAX_STANDARD_TX_SIZE) || + (!sign && static_cast(nBytes) > MAX_STANDARD_TX_SIZE)) { + error = _("Transaction too large"); + return false; + } + } + + if (fee_needed > nFeeRet) { + error = _("Fee needed > fee paid"); + return false; + } + + if (nFeeRet > m_default_max_tx_fee) { + error = TransactionErrorString(TransactionError::MAX_FEE_EXCEEDED); + return false; + } + + if (gArgs.GetBoolArg("-walletrejectlongchains", DEFAULT_WALLET_REJECT_LONG_CHAINS)) { + // Lastly, ensure this tx will pass the mempool's chain limits + if (!chain().checkChainLimits(tx)) { + error = _("Transaction has too long of a mempool chain"); + return false; + } + } + + // Before we return success, we assume any change key will be used to prevent + // accidental re-use. + reservedest.KeepDestination(); + fee_calc_out = feeCalc; + + WalletLogPrintf("Fee Calculation: Fee:%d Bytes:%u Tgt:%d (requested %d) Reason:\"%s\" Decay %.5f: Estimation: (%g - %g) %.2f%% %.1f/(%.1f %d mem %.1f out) Fail: (%g - %g) %.2f%% %.1f/(%.1f %d mem %.1f out)\n", + nFeeRet, nBytes, feeCalc.returnedTarget, feeCalc.desiredTarget, StringForFeeReason(feeCalc.reason), feeCalc.est.decay, + feeCalc.est.pass.start, feeCalc.est.pass.end, + (feeCalc.est.pass.totalConfirmed + feeCalc.est.pass.inMempool + feeCalc.est.pass.leftMempool) > 0.0 ? 100 * feeCalc.est.pass.withinTarget / (feeCalc.est.pass.totalConfirmed + feeCalc.est.pass.inMempool + feeCalc.est.pass.leftMempool) : 0.0, + feeCalc.est.pass.withinTarget, feeCalc.est.pass.totalConfirmed, feeCalc.est.pass.inMempool, feeCalc.est.pass.leftMempool, + feeCalc.est.fail.start, feeCalc.est.fail.end, + (feeCalc.est.fail.totalConfirmed + feeCalc.est.fail.inMempool + feeCalc.est.fail.leftMempool) > 0.0 ? 100 * feeCalc.est.fail.withinTarget / (feeCalc.est.fail.totalConfirmed + feeCalc.est.fail.inMempool + feeCalc.est.fail.leftMempool) : 0.0, + feeCalc.est.fail.withinTarget, feeCalc.est.fail.totalConfirmed, feeCalc.est.fail.inMempool, feeCalc.est.fail.leftMempool); + return true; +} + +bool CWallet::CreateTransaction( + const std::vector& vecSend, + CTransactionRef& tx, + CAmount& nFeeRet, + int& nChangePosInOut, + bilingual_str& error, + const CCoinControl& coin_control, + FeeCalculation& fee_calc_out, + bool sign, + int nExtraPayloadSize) +{ + int nChangePosIn = nChangePosInOut; + Assert(!tx); // tx is an out-param. TODO change the return type from bool to tx (or nullptr) + bool res = CreateTransactionInternal(vecSend, tx, nFeeRet, nChangePosInOut, error, coin_control, fee_calc_out, sign, nExtraPayloadSize); + // try with avoidpartialspends unless it's enabled already + if (res && nFeeRet > 0 /* 0 means non-functional fee rate estimation */ && m_max_aps_fee > -1 && !coin_control.m_avoid_partial_spends) { + CCoinControl tmp_cc = coin_control; + tmp_cc.m_avoid_partial_spends = true; + + // Re-use the change destination from the first creation attempt to avoid skipping BIP44 indexes + const int ungrouped_change_pos = nChangePosInOut; + if (ungrouped_change_pos != -1) { + ExtractDestination(tx->vout[ungrouped_change_pos].scriptPubKey, tmp_cc.destChange); + } + + CAmount nFeeRet2; + CTransactionRef tx2; + int nChangePosInOut2 = nChangePosIn; + bilingual_str error2; // fired and forgotten; if an error occurs, we discard the results + if (CreateTransactionInternal(vecSend, tx2, nFeeRet2, nChangePosInOut2, error2, tmp_cc, fee_calc_out, sign, nExtraPayloadSize)) { + // if fee of this alternative one is within the range of the max fee, we use this one + const bool use_aps = nFeeRet2 <= nFeeRet + m_max_aps_fee; + WalletLogPrintf("Fee non-grouped = %lld, grouped = %lld, using %s\n", nFeeRet, nFeeRet2, use_aps ? "grouped" : "non-grouped"); + if (use_aps) { + tx = tx2; + nFeeRet = nFeeRet2; + nChangePosInOut = nChangePosInOut2; + } + } + } + return res; +} + +bool CWallet::FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, int& nChangePosInOut, bilingual_str& error, bool lockUnspents, const std::set& setSubtractFeeFromOutputs, CCoinControl coinControl) +{ + std::vector vecSend; + + // If no specific change position was requested, apply BIP69 + if (nChangePosInOut == -1) { + std::sort(tx.vin.begin(), tx.vin.end(), CompareInputBIP69()); + std::sort(tx.vout.begin(), tx.vout.end(), CompareOutputBIP69()); + } + + // Turn the txout set into a CRecipient vector. + for (size_t idx = 0; idx < tx.vout.size(); idx++) { + const CTxOut& txOut = tx.vout[idx]; + CRecipient recipient = {txOut.scriptPubKey, txOut.nValue, setSubtractFeeFromOutputs.count(idx) == 1}; + vecSend.push_back(recipient); + } + + coinControl.fAllowOtherInputs = true; + + for (const CTxIn& txin : tx.vin) { + coinControl.Select(txin.prevout); + } + + // Acquire the locks to prevent races to the new locked unspents between the + // CreateTransaction call and LockCoin calls (when lockUnspents is true). + LOCK(cs_wallet); + + CTransactionRef tx_new; + FeeCalculation fee_calc_out; + if (!CreateTransaction(vecSend, tx_new, nFeeRet, nChangePosInOut, error, coinControl, fee_calc_out, false, tx.vExtraPayload.size())) { + return false; + } + + if (nChangePosInOut != -1) { + tx.vout.insert(tx.vout.begin() + nChangePosInOut, tx_new->vout[nChangePosInOut]); + } + + // Copy output sizes from new transaction; they may have had the fee + // subtracted from them. + for (unsigned int idx = 0; idx < tx.vout.size(); idx++) { + tx.vout[idx].nValue = tx_new->vout[idx].nValue; + } + + // Add new txins while keeping original txin scriptSig/order. + for (const CTxIn& txin : tx_new->vin) { + if (!coinControl.IsSelected(txin.prevout)) { + tx.vin.push_back(txin); + + } + if (lockUnspents) { + LockCoin(txin.prevout); + } + + } + + return true; +} diff --git a/src/wallet/spend.h b/src/wallet/spend.h new file mode 100644 index 0000000000000..03f9a7c2b5023 --- /dev/null +++ b/src/wallet/spend.h @@ -0,0 +1,64 @@ +// Copyright (c) 2021 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_SPEND_H +#define BITCOIN_WALLET_SPEND_H + +#include +#include +#include + +class COutput +{ +public: + const CWalletTx *tx; + + /** Index in tx->vout. */ + int i; + + /** + * Depth in block chain. + * If > 0: the tx is on chain and has this many confirmations. + * If = 0: the tx is waiting confirmation. + * If < 0: a conflicting tx is on chain and has this many confirmations. */ + int nDepth; + + /** 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 nInputBytes; + + /** Whether we have the private keys to spend this output */ + bool fSpendable; + + /** Whether we know how to spend this output, ignoring the lack of keys */ + bool fSolvable; + + /** Whether to use the maximum sized, 72 byte signature when calculating the size of the input spend. This should only be set when watch-only outputs are allowed */ + bool use_max_sig; + + /** + * Whether this output is considered safe to spend. Unconfirmed transactions + * from outside keys and unconfirmed replacement transactions are considered + * unsafe and will not be used to fund new spending transactions. + */ + bool fSafe; + + COutput(const CWalletTx *txIn, int iIn, int nDepthIn, bool fSpendableIn, bool fSolvableIn, bool fSafeIn, bool use_max_sig_in = false) + { + tx = txIn; i = iIn; nDepth = nDepthIn; fSpendable = fSpendableIn; fSolvable = fSolvableIn; fSafe = fSafeIn; nInputBytes = -1; use_max_sig = use_max_sig_in; + // If known and signable by the given wallet, compute nInputBytes + // Failure will keep this value -1 + if (fSpendable && tx) { + nInputBytes = tx->GetSpendSize(i, use_max_sig); + } + } + + std::string ToString() const; + + inline CInputCoin GetInputCoin() const + { + return CInputCoin(tx->tx, i, nInputBytes); + } +}; + +#endif // BITCOIN_WALLET_SPEND_H diff --git a/src/wallet/transaction.cpp b/src/wallet/transaction.cpp new file mode 100644 index 0000000000000..cf98b516f1773 --- /dev/null +++ b/src/wallet/transaction.cpp @@ -0,0 +1,25 @@ +// Copyright (c) 2021 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 + +bool CWalletTx::IsEquivalentTo(const CWalletTx& _tx) const +{ + CMutableTransaction tx1 {*this->tx}; + CMutableTransaction tx2 {*_tx.tx}; + for (auto& txin : tx1.vin) txin.scriptSig = CScript(); + for (auto& txin : tx2.vin) txin.scriptSig = CScript(); + return CTransaction(tx1) == CTransaction(tx2); +} + +bool CWalletTx::InMempool() const +{ + return fInMempool; +} + +int64_t CWalletTx::GetTxTime() const +{ + int64_t n = nTimeSmart; + return n ? n : nTimeReceived; +} diff --git a/src/wallet/transaction.h b/src/wallet/transaction.h new file mode 100644 index 0000000000000..38549aa11f7af --- /dev/null +++ b/src/wallet/transaction.h @@ -0,0 +1,383 @@ +// Copyright (c) 2021 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_TRANSACTION_H +#define BITCOIN_WALLET_TRANSACTION_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +class CCoinControl; +struct bilingual_str; +struct COutputEntry; + +typedef std::map mapValue_t; + +//Get the marginal bytes of spending the specified output +int CalculateMaximumSignedInputSize(const CTxOut& txout, const CWallet* pwallet, bool use_max_sig = false); + +static inline void ReadOrderPos(int64_t& nOrderPos, mapValue_t& mapValue) +{ + if (!mapValue.count("n")) + { + nOrderPos = -1; // TODO: calculate elsewhere + return; + } + nOrderPos = LocaleIndependentAtoi(mapValue["n"]); +} + +static inline void WriteOrderPos(const int64_t& nOrderPos, mapValue_t& mapValue) +{ + if (nOrderPos == -1) + return; + mapValue["n"] = ToString(nOrderPos); +} + +/** Legacy class used for deserializing vtxPrev for backwards compatibility. + * vtxPrev was removed in commit 93a18a3650292afbb441a47d1fa1b94aeb0164e3, + * but old wallet.dat files may still contain vtxPrev vectors of CMerkleTxs. + * These need to get deserialized for field alignment when deserializing + * a CWalletTx, but the deserialized values are discarded.**/ +class CMerkleTx +{ +public: + template + void Unserialize(Stream& s) + { + CTransactionRef tx; + uint256 hashBlock; + std::vector vMerkleBranch; + int nIndex; + + s >> tx >> hashBlock >> vMerkleBranch >> nIndex; + } +}; + +/** + * A transaction with a bunch of additional info that only the owner cares about. + * It includes any unrecorded transactions needed to link it back to the block chain. + */ +class CWalletTx +{ +private: + const CWallet* const pwallet; + + /** Constant used in hashBlock to indicate tx has been abandoned, only used at + * serialization/deserialization to avoid ambiguity with conflicted. + */ + static constexpr const uint256& ABANDON_HASH = uint256::ONE; + + mutable bool fIsChainlocked{false}; + mutable bool fIsInstantSendLocked{false}; + +public: + /** + * Key/value map with information about the transaction. + * + * The following keys can be read and written through the map and are + * serialized in the wallet database: + * + * "comment", "to" - comment strings provided to sendtoaddress, + * and sendmany wallet RPCs + * "replaces_txid" - txid (as HexStr) of transaction replaced by + * bumpfee on transaction created by bumpfee + * "replaced_by_txid" - txid (as HexStr) of transaction created by + * bumpfee on transaction replaced by bumpfee + * "from", "message" - obsolete fields that could be set in UI prior to + * 2011 (removed in commit 4d9b223) + * + * The following keys are serialized in the wallet database, but shouldn't + * be read or written through the map (they will be temporarily added and + * removed from the map during serialization): + * + * "fromaccount" - serialized strFromAccount value + * "n" - serialized nOrderPos value + * "timesmart" - serialized nTimeSmart value + * "spent" - serialized vfSpent value that existed prior to + * 2014 (removed in commit 93a18a3) + */ + mapValue_t mapValue; + std::vector > vOrderForm; + unsigned int fTimeReceivedIsTxTime; + unsigned int nTimeReceived; //!< time received by this node + /** + * Stable timestamp that never changes, and reflects the order a transaction + * was added to the wallet. Timestamp is based on the block time for a + * transaction added as part of a block, or else the time when the + * transaction was received if it wasn't part of a block, with the timestamp + * adjusted in both cases so timestamp order matches the order transactions + * were added to the wallet. More details can be found in + * CWallet::ComputeTimeSmart(). + */ + unsigned int nTimeSmart; + /** + * From me flag is set to 1 for transactions that were created by the wallet + * on this bitcoin node, and set to 0 for transactions that were created + * externally and came in through the network or sendrawtransaction RPC. + */ + bool fFromMe; + int64_t nOrderPos; //!< position in ordered transaction list + std::multimap::const_iterator m_it_wtxOrdered; + + // memory only + enum AmountType { DEBIT, CREDIT, IMMATURE_CREDIT, AVAILABLE_CREDIT, ANON_CREDIT, DENOM_UCREDIT, DENOM_CREDIT, AMOUNTTYPE_ENUM_ELEMENTS }; + CAmount GetCachableAmount(AmountType type, const isminefilter& filter, bool recalculate = false) const; + mutable CachableAmount m_amounts[AMOUNTTYPE_ENUM_ELEMENTS]; + /** + * This flag is true if all m_amounts caches are empty. This is particularly + * useful in places where MarkDirty is conditionally called and the + * condition can be expensive and thus can be skipped if the flag is true. + * See MarkDestinationsDirty. + */ + mutable bool m_is_cache_empty{true}; + mutable bool fChangeCached; + mutable bool fInMempool; + mutable CAmount nChangeCached; + + CWalletTx(const CWallet* wallet, CTransactionRef arg) + : pwallet(wallet), + tx(std::move(arg)) + { + Init(); + } + + void Init() + { + mapValue.clear(); + vOrderForm.clear(); + fTimeReceivedIsTxTime = false; + nTimeReceived = 0; + nTimeSmart = 0; + fFromMe = false; + fChangeCached = false; + fInMempool = false; + nChangeCached = 0; + nOrderPos = -1; + m_confirm = Confirmation{}; + } + + CTransactionRef tx; + + /** New transactions start as UNCONFIRMED. At BlockConnected, + * they will transition to CONFIRMED. In case of reorg, at BlockDisconnected, + * they roll back to UNCONFIRMED. If we detect a conflicting transaction at + * block connection, we update conflicted tx and its dependencies as CONFLICTED. + * If tx isn't confirmed and outside of mempool, the user may switch it to ABANDONED + * by using the abandontransaction call. This last status may be override by a CONFLICTED + * or CONFIRMED transition. + */ + enum Status { + UNCONFIRMED, + CONFIRMED, + CONFLICTED, + ABANDONED + }; + + /** Confirmation includes tx status and a triplet of {block height/block hash/tx index in block} + * at which tx has been confirmed. All three are set to 0 if tx is unconfirmed or abandoned. + * Meaning of these fields changes with CONFLICTED state where they instead point to block hash + * and block height of the deepest conflicting tx. + */ + struct Confirmation { + Status status; + int block_height; + uint256 hashBlock; + int nIndex; + Confirmation(Status s = UNCONFIRMED, int b = 0, uint256 h = uint256(), int i = 0) : status(s), block_height(b), hashBlock(h), nIndex(i) {} + }; + + Confirmation m_confirm; + + template + void Serialize(Stream& s) const + { + mapValue_t mapValueCopy = mapValue; + + mapValueCopy["fromaccount"] = ""; + WriteOrderPos(nOrderPos, mapValueCopy); + if (nTimeSmart) { + mapValueCopy["timesmart"] = strprintf("%u", nTimeSmart); + } + + std::vector dummy_vector1; //!< Used to be vMerkleBranch + std::vector dummy_vector2; //!< Used to be vtxPrev + bool dummy_bool = false; //!< Used to be fSpent + uint256 serializedHash = isAbandoned() ? ABANDON_HASH : m_confirm.hashBlock; + int serializedIndex = isAbandoned() || isConflicted() ? -1 : m_confirm.nIndex; + s << tx << serializedHash << dummy_vector1 << serializedIndex << dummy_vector2 << mapValueCopy << vOrderForm << fTimeReceivedIsTxTime << nTimeReceived << fFromMe << dummy_bool; + } + + template + void Unserialize(Stream& s) + { + Init(); + + std::vector dummy_vector1; //!< Used to be vMerkleBranch + std::vector dummy_vector2; //!< Used to be vtxPrev + bool dummy_bool; //! Used to be fSpent + int serializedIndex; + s >> tx >> m_confirm.hashBlock >> dummy_vector1 >> serializedIndex >> dummy_vector2 >> mapValue >> vOrderForm >> fTimeReceivedIsTxTime >> nTimeReceived >> fFromMe >> dummy_bool; + + /* At serialization/deserialization, an nIndex == -1 means that hashBlock refers to + * the earliest block in the chain we know this or any in-wallet ancestor conflicts + * with. If nIndex == -1 and hashBlock is ABANDON_HASH, it means transaction is abandoned. + * In same context, an nIndex >= 0 refers to a confirmed transaction (if hashBlock set) or + * unconfirmed one. Older clients interpret nIndex == -1 as unconfirmed for backward + * compatibility (pre-commit 9ac63d6). + */ + if (serializedIndex == -1 && m_confirm.hashBlock == ABANDON_HASH) { + setAbandoned(); + } else if (serializedIndex == -1) { + setConflicted(); + } else if (!m_confirm.hashBlock.IsNull()) { + m_confirm.nIndex = serializedIndex; + setConfirmed(); + } + + ReadOrderPos(nOrderPos, mapValue); + nTimeSmart = mapValue.count("timesmart") ? (unsigned int)LocaleIndependentAtoi(mapValue["timesmart"]) : 0; + + mapValue.erase("fromaccount"); + mapValue.erase("spent"); + mapValue.erase("n"); + mapValue.erase("timesmart"); + } + + void SetTx(CTransactionRef arg) + { + tx = std::move(arg); + } + + //! make sure balances are recalculated + void MarkDirty() + { + m_amounts[DEBIT].Reset(); + m_amounts[CREDIT].Reset(); + m_amounts[ANON_CREDIT].Reset(); + m_amounts[DENOM_CREDIT].Reset(); + m_amounts[DENOM_UCREDIT].Reset(); + m_amounts[IMMATURE_CREDIT].Reset(); + m_amounts[AVAILABLE_CREDIT].Reset(); + fChangeCached = false; + m_is_cache_empty = true; + } + + const CWallet* GetWallet() const + { + return pwallet; + } + + //! filter decides which addresses will count towards the debit + CAmount GetDebit(const isminefilter& filter) const; + CAmount GetCredit(const isminefilter& filter) const; + CAmount GetImmatureCredit(bool fUseCache = true) const; + // TODO: Remove "NO_THREAD_SAFETY_ANALYSIS" and replace it with the correct + // annotation "EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet)". The + // annotation "NO_THREAD_SAFETY_ANALYSIS" was temporarily added to avoid + // having to resolve the issue of member access into incomplete type CWallet. + CAmount GetAvailableCredit(bool fUseCache = true, const isminefilter& filter = ISMINE_SPENDABLE) const NO_THREAD_SAFETY_ANALYSIS; + CAmount GetImmatureWatchOnlyCredit(const bool fUseCache = true) const; + CAmount GetChange() const; + + // TODO: Remove "NO_THREAD_SAFETY_ANALYSIS" and replace it with the correct + // annotation "EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet)". The + // annotation "NO_THREAD_SAFETY_ANALYSIS" was temporarily added to avoid + // having to resolve the issue of member access into incomplete type CWallet. + CAmount GetAnonymizedCredit(const CCoinControl* coinControl = nullptr) const NO_THREAD_SAFETY_ANALYSIS; + CAmount GetDenominatedCredit(bool unconfirmed, bool fUseCache=true) const NO_THREAD_SAFETY_ANALYSIS; + + /** Get the marginal bytes if spending the specified output from this transaction */ + int GetSpendSize(unsigned int out, bool use_max_sig = false) const + { + return CalculateMaximumSignedInputSize(tx->vout[out], pwallet, use_max_sig); + } + + void GetAmounts(std::list& listReceived, + std::list& listSent, CAmount& nFee, const isminefilter& filter) const; + + bool IsFromMe(const isminefilter& filter) const + { + return (GetDebit(filter) > 0); + } + + /** True if only scriptSigs are different */ + bool IsEquivalentTo(const CWalletTx& tx) const; + + bool InMempool() const; + bool IsTrusted() const; + + int64_t GetTxTime() const; + + bool CanBeResent() const; + + /** Pass this transaction to node for mempool insertion and relay to peers if flag set to true */ + bool SubmitMemoryPoolAndRelay(bilingual_str& err_string, bool relay); + + // TODO: Remove "NO_THREAD_SAFETY_ANALYSIS" and replace it with the correct + // annotation "EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet)". The annotation + // "NO_THREAD_SAFETY_ANALYSIS" was temporarily added to avoid having to + // resolve the issue of member access into incomplete type CWallet. Note + // that we still have the runtime check "AssertLockHeld(pwallet->cs_wallet)" + // in place. + std::set GetConflicts() const NO_THREAD_SAFETY_ANALYSIS; + + /** + * Return depth of transaction in blockchain: + * <0 : conflicts with a transaction this deep in the blockchain + * 0 : in memory pool, waiting to be included in a block + * >=1 : this many blocks deep in the main chain + */ + // TODO: Remove "NO_THREAD_SAFETY_ANALYSIS" and replace it with the correct + // annotation "EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet)". The annotation + // "NO_THREAD_SAFETY_ANALYSIS" was temporarily added to avoid having to + // resolve the issue of member access into incomplete type CWallet. Note + // that we still have the runtime check "AssertLockHeld(pwallet->cs_wallet)" + // in place. + int GetDepthInMainChain() const NO_THREAD_SAFETY_ANALYSIS; + bool IsInMainChain() const { return GetDepthInMainChain() > 0; } + bool IsLockedByInstantSend() const; + bool IsChainLocked() const NO_THREAD_SAFETY_ANALYSIS; + + /** + * @return number of blocks to maturity for this transaction: + * 0 : is not a coinbase transaction, or is a mature coinbase transaction + * >0 : is a coinbase transaction which matures in this many blocks + */ + int GetBlocksToMaturity() const; + bool isAbandoned() const { return m_confirm.status == CWalletTx::ABANDONED; } + void setAbandoned() + { + m_confirm.status = CWalletTx::ABANDONED; + m_confirm.hashBlock = uint256(); + m_confirm.block_height = 0; + m_confirm.nIndex = 0; + } + bool isConflicted() const { return m_confirm.status == CWalletTx::CONFLICTED; } + void setConflicted() { m_confirm.status = CWalletTx::CONFLICTED; } + bool isUnconfirmed() const { return m_confirm.status == CWalletTx::UNCONFIRMED; } + void setUnconfirmed() { m_confirm.status = CWalletTx::UNCONFIRMED; } + bool isConfirmed() const { return m_confirm.status == CWalletTx::CONFIRMED; } + void setConfirmed() { m_confirm.status = CWalletTx::CONFIRMED; } + const uint256& GetHash() const { return tx->GetHash(); } + bool IsCoinBase() const { return tx->IsCoinBase(); } + bool IsPlatformTransfer() const { return tx->IsPlatformTransfer(); } + bool IsImmatureCoinBase() const; + + // Disable copying of CWalletTx objects to prevent bugs where instances get + // copied in and out of the mapWallet map, and fields are updated in the + // wrong copy. + CWalletTx(CWalletTx const &) = delete; + void operator=(CWalletTx const &x) = delete; +}; + +#endif // BITCOIN_WALLET_TRANSACTION_H diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 371bc78a31760..d607311a470c6 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -62,8 +62,6 @@ const std::map WALLET_FLAG_CAVEATS{ }, }; -static constexpr size_t OUTPUT_GROUP_MAX_ENTRIES{100}; - RecursiveMutex cs_wallets; static std::vector> vpwallets GUARDED_BY(cs_wallets); static std::list g_load_wallet_fns GUARDED_BY(cs_wallets); @@ -406,11 +404,6 @@ std::shared_ptr RestoreWallet(interfaces::Chain& chain, interfaces::Coi * @{ */ -std::string COutput::ToString() const -{ - return strprintf("COutput(%s, %d, %d) [%s]", tx->GetHash().ToString(), i, nDepth, FormatMoney(tx->tx->vout[i].nValue)); -} - const CWalletTx* CWallet::GetWalletTx(const uint256& hash) const { AssertLockHeld(cs_wallet); @@ -1381,20 +1374,6 @@ void CWallet::BlockUntilSyncedToCurrentChain() const { chain().waitForNotificationsIfTipChanged(last_block_hash); } - -isminetype CWallet::IsMine(const CTxIn &txin) const -{ - AssertLockHeld(cs_wallet); - std::map::const_iterator mi = mapWallet.find(txin.prevout.hash); - if (mi != mapWallet.end()) - { - const CWalletTx& prev = (*mi).second; - if (txin.prevout.n < prev.tx->vout.size()) - return IsMine(prev.tx->vout[txin.prevout.n]); - } - return ISMINE_NO; -} - // Note that this function doesn't distinguish between a 0-valued input, // and a not-"is mine" (according to the filter) input. CAmount CWallet::GetDebit(const CTxIn &txin, const isminefilter& filter) const @@ -1579,49 +1558,6 @@ isminetype CWallet::IsMine(const CScript& script) const return result; } -CAmount CWallet::GetCredit(const CTxOut& txout, const isminefilter& filter) const -{ - if (!MoneyRange(txout.nValue)) - throw std::runtime_error(std::string(__func__) + ": value out of range"); - LOCK(cs_wallet); - return ((IsMine(txout) & filter) ? txout.nValue : 0); -} - -bool CWallet::IsChange(const CTxOut& txout) const -{ - return IsChange(txout.scriptPubKey); -} - -bool CWallet::IsChange(const CScript& script) const -{ - // TODO: fix handling of 'change' outputs. The assumption is that any - // payment to a script that is ours, but is not in the address book - // is change. That assumption is likely to break when we implement multisignature - // wallets that return change back into a multi-signature-protected address; - // a better way of identifying which outputs are 'the send' and which are - // 'the change' will need to be implemented (maybe extend CWalletTx to remember - // which output, if any, was change). - AssertLockHeld(cs_wallet); - if (IsMine(script)) - { - CTxDestination address; - if (!ExtractDestination(script, address)) - return true; - if (!FindAddressBookEntry(address)) { - return true; - } - } - return false; -} - -CAmount CWallet::GetChange(const CTxOut& txout) const -{ - AssertLockHeld(cs_wallet); - if (!MoneyRange(txout.nValue)) - throw std::runtime_error(std::string(__func__) + ": value out of range"); - return (IsChange(txout) ? txout.nValue : 0); -} - bool CWallet::IsMine(const CTransaction& tx) const { AssertLockHeld(cs_wallet); @@ -1648,52 +1584,6 @@ CAmount CWallet::GetDebit(const CTransaction& tx, const isminefilter& filter) co return nDebit; } -bool CWallet::IsAllFromMe(const CTransaction& tx, const isminefilter& filter) const -{ - LOCK(cs_wallet); - - for (const CTxIn& txin : tx.vin) - { - auto mi = mapWallet.find(txin.prevout.hash); - if (mi == mapWallet.end()) - return false; // any unknown inputs can't be from us - - const CWalletTx& prev = (*mi).second; - - if (txin.prevout.n >= prev.tx->vout.size()) - return false; // invalid input! - - if (!(IsMine(prev.tx->vout[txin.prevout.n]) & filter)) - return false; - } - return true; -} - -CAmount CWallet::GetCredit(const CTransaction& tx, const isminefilter& filter) const -{ - CAmount nCredit = 0; - for (const CTxOut& txout : tx.vout) - { - nCredit += GetCredit(txout, filter); - if (!MoneyRange(nCredit)) - throw std::runtime_error(std::string(__func__) + ": value out of range"); - } - return nCredit; -} - -CAmount CWallet::GetChange(const CTransaction& tx) const -{ - LOCK(cs_wallet); - CAmount nChange = 0; - for (const CTxOut& txout : tx.vout) - { - nChange += GetChange(txout); - if (!MoneyRange(nChange)) - throw std::runtime_error(std::string(__func__) + ": value out of range"); - } - return nChange; -} - bool CWallet::IsHDEnabled() const { // All Active ScriptPubKeyMans must be HD for this to be true @@ -1789,12 +1679,6 @@ bool CWallet::AddWalletFlags(uint64_t flags) return LoadWalletFlags(flags); } -int64_t CWalletTx::GetTxTime() const -{ - int64_t n = nTimeSmart; - return n ? n : nTimeReceived; -} - // Helper for producing a max-sized low-S low-R signature (eg 71 bytes) // or a max-sized low-S signature (e.g. 72 bytes) if use_max_sig is true bool CWallet::DummySignInput(CTxIn &tx_in, const CTxOut &txout, bool use_max_sig) const @@ -1885,97 +1769,6 @@ bool CWallet::ImportScriptPubKeys(const std::string& label, const std::set txouts; - for (const CTxIn& input : tx.vin) { - const auto mi = wallet->mapWallet.find(input.prevout.hash); - // Can not estimate size without knowing the input details - if (mi == wallet->mapWallet.end()) { - return -1; - } - assert(input.prevout.n < mi->second.tx->vout.size()); - txouts.emplace_back(mi->second.tx->vout[input.prevout.n]); - } - return CalculateMaximumSignedTxSize(tx, wallet, txouts, use_max_sig); -} - -// txouts needs to be in the order of tx.vin -int64_t CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *wallet, const std::vector& txouts, bool use_max_sig) -{ - CMutableTransaction txNew(tx); - if (!wallet->DummySignTx(txNew, txouts, use_max_sig)) { - return -1; - } - return ::GetSerializeSize(txNew, PROTOCOL_VERSION); -} - -int CalculateMaximumSignedInputSize(const CTxOut& txout, const CWallet* wallet, bool use_max_sig) -{ - CMutableTransaction txn; - txn.vin.push_back(CTxIn(COutPoint())); - if (!wallet->DummySignInput(txn.vin[0], txout, use_max_sig)) { - return -1; - } - return ::GetSerializeSize(txn.vin[0], PROTOCOL_VERSION); -} - -void CWalletTx::GetAmounts(std::list& listReceived, - std::list& listSent, CAmount& nFee, const isminefilter& filter) const -{ - nFee = 0; - listReceived.clear(); - listSent.clear(); - - // Compute fee: - CAmount nDebit = GetDebit(filter); - if (nDebit > 0) // debit>0 means we signed/sent this transaction - { - CAmount nValueOut = tx->GetValueOut(); - nFee = nDebit - nValueOut; - } - - LOCK(pwallet->cs_wallet); - // Sent/received. - for (unsigned int i = 0; i < tx->vout.size(); ++i) - { - const CTxOut& txout = tx->vout[i]; - isminetype fIsMine = pwallet->IsMine(txout); - // Only need to handle txouts if AT LEAST one of these is true: - // 1) they debit from us (sent) - // 2) the output is to us (received) - if (nDebit > 0) - { - // Don't report 'change' txouts - if (pwallet->IsChange(txout)) - continue; - } - else if (!(fIsMine & filter)) - continue; - - // In either case, we need to get the destination address - CTxDestination address; - - if (!ExtractDestination(txout.scriptPubKey, address) && !txout.scriptPubKey.IsUnspendable()) - { - pwallet->WalletLogPrintf("CWalletTx::GetAmounts: Unknown transaction type found, txid %s\n", - this->GetHash().ToString()); - address = CNoDestination(); - } - - COutputEntry output = {address, txout.nValue, (int)i}; - - // If we are debited by the transaction, add the output as a "sent" entry - if (nDebit > 0) - listSent.push_back(output); - - // If we are receiving the output, add it as a "received" entry - if (fIsMine & filter) - listReceived.push_back(output); - } - -} - /** * Scan active chain for relevant transactions after importing keys. This should * be called whenever new keys are added to the wallet, with the oldest key @@ -2210,104 +2003,6 @@ std::set CWalletTx::GetConflicts() const return result; } -CAmount CWalletTx::GetCachableAmount(AmountType type, const isminefilter& filter, bool recalculate) const -{ - auto& amount = m_amounts[type]; - if (recalculate || !amount.m_cached[filter]) { - amount.Set(filter, type == DEBIT ? pwallet->GetDebit(*tx, filter) : pwallet->GetCredit(*tx, filter)); - m_is_cache_empty = false; - } - return amount.m_value[filter]; -} - -CAmount CWalletTx::GetDebit(const isminefilter& filter) const -{ - if (tx->vin.empty()) - return 0; - - CAmount debit = 0; - if (filter & ISMINE_SPENDABLE) { - debit += GetCachableAmount(DEBIT, ISMINE_SPENDABLE); - } - if (filter & ISMINE_WATCH_ONLY) { - debit += GetCachableAmount(DEBIT, ISMINE_WATCH_ONLY); - } - return debit; -} - -CAmount CWalletTx::GetCredit(const isminefilter& filter) const -{ - // Must wait until coinbase is safely deep enough in the chain before valuing it - if (IsImmatureCoinBase()) - return 0; - - CAmount credit = 0; - if (filter & ISMINE_SPENDABLE) { - // GetBalance can assume transactions in mapWallet won't change - credit += GetCachableAmount(CREDIT, ISMINE_SPENDABLE); - } - if (filter & ISMINE_WATCH_ONLY) { - credit += GetCachableAmount(CREDIT, ISMINE_WATCH_ONLY); - } - return credit; -} - -CAmount CWalletTx::GetImmatureCredit(bool fUseCache) const -{ - if (IsImmatureCoinBase() && IsInMainChain()) { - return GetCachableAmount(IMMATURE_CREDIT, ISMINE_SPENDABLE, !fUseCache); - } - - return 0; -} - -CAmount CWalletTx::GetAvailableCredit(bool fUseCache, const isminefilter& filter) const -{ - if (pwallet == nullptr) - return 0; - - // Avoid caching ismine for NO or ALL cases (could remove this check and simplify in the future). - bool allow_cache = (filter & ISMINE_ALL) && (filter & ISMINE_ALL) != ISMINE_ALL; - - // Must wait until coinbase is safely deep enough in the chain before valuing it - if (IsImmatureCoinBase()) - return 0; - - if (fUseCache && allow_cache && m_amounts[AVAILABLE_CREDIT].m_cached[filter]) { - return m_amounts[AVAILABLE_CREDIT].m_value[filter]; - } - - bool allow_used_addresses = (filter & ISMINE_USED) || !pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE); - CAmount nCredit = 0; - uint256 hashTx = GetHash(); - for (unsigned int i = 0; i < tx->vout.size(); i++) - { - if (!pwallet->IsSpent(hashTx, i) && (allow_used_addresses || !pwallet->IsSpentKey(hashTx, i))) - { - const CTxOut &txout = tx->vout[i]; - nCredit += pwallet->GetCredit(txout, filter); - if (!MoneyRange(nCredit)) - throw std::runtime_error(std::string(__func__) + ": value out of range"); - } - } - - if (allow_cache) { - m_amounts[AVAILABLE_CREDIT].Set(filter, nCredit); - m_is_cache_empty = false; - } - - return nCredit; -} - -CAmount CWalletTx::GetImmatureWatchOnlyCredit(const bool fUseCache) const -{ - if (IsImmatureCoinBase() && IsInMainChain()) { - return GetCachableAmount(IMMATURE_CREDIT, ISMINE_WATCH_ONLY, !fUseCache); - } - - return 0; -} - CAmount CWalletTx::GetAnonymizedCredit(const CCoinControl* coinControl) const { if (!pwallet) @@ -2395,69 +2090,6 @@ CAmount CWalletTx::GetDenominatedCredit(bool unconfirmed, bool fUseCache) const return nCredit; } -CAmount CWalletTx::GetChange() const -{ - if (fChangeCached) - return nChangeCached; - nChangeCached = pwallet->GetChange(*tx); - fChangeCached = true; - return nChangeCached; -} - -bool CWalletTx::InMempool() const -{ - return fInMempool; -} - -bool CWalletTx::IsTrusted() const -{ - std::set trusted_parents; - LOCK(pwallet->cs_wallet); - return pwallet->IsTrusted(*this, trusted_parents); -} - -bool CWallet::IsTrusted(const CWalletTx& wtx, std::set& trusted_parents) const -{ - AssertLockHeld(cs_wallet); - // Quick answer in most cases - if (!chain().checkFinalTx(*wtx.tx)) return false; - int nDepth = wtx.GetDepthInMainChain(); - if (nDepth >= 1) return true; - if (nDepth < 0) return false; - if (wtx.IsLockedByInstantSend()) return true; - // using wtx's cached debit - if (!m_spend_zero_conf_change || !wtx.IsFromMe(ISMINE_ALL)) return false; - - // Don't trust unconfirmed transactions from us unless they are in the mempool. - if (!wtx.InMempool()) return false; - - // Trusted if all inputs are from us and are in the mempool: - for (const CTxIn& txin : wtx.tx->vin) - { - // Transactions not sent by us: not trusted - const CWalletTx* parent = GetWalletTx(txin.prevout.hash); - if (parent == nullptr) return false; - const CTxOut& parentOut = parent->tx->vout[txin.prevout.n]; - // Check that this specific input being spent is trusted - if (IsMine(parentOut) != ISMINE_SPENDABLE) return false; - // If we've already trusted this parent, continue - if (trusted_parents.count(parent->GetHash())) continue; - // Recurse to check that the parent is also trusted - if (!IsTrusted(*parent, trusted_parents)) return false; - trusted_parents.insert(parent->GetHash()); - } - return true; -} - -bool CWalletTx::IsEquivalentTo(const CWalletTx& _tx) const -{ - CMutableTransaction tx1 {*this->tx}; - CMutableTransaction tx2 {*_tx.tx}; - for (auto& txin : tx1.vin) txin.scriptSig = CScript(); - for (auto& txin : tx2.vin) txin.scriptSig = CScript(); - return CTransaction(tx1) == CTransaction(tx2); -} - // Rebroadcast transactions from the wallet. We do this on a random timer // to slightly obfuscate which transactions come from our wallet. // @@ -2518,7 +2150,6 @@ void MaybeResendWalletTxs() * @{ */ - std::unordered_set CWallet::GetSpendableTXs() const { AssertLockHeld(cs_wallet); @@ -2540,40 +2171,6 @@ std::unordered_set CWallet::GetSpendableTXs() return ret; } -CWallet::Balance CWallet::GetBalance(const int min_depth, const bool avoid_reuse, const bool fAddLocked, const CCoinControl* coinControl) const -{ - Balance ret; - isminefilter reuse_filter = avoid_reuse ? ISMINE_NO : ISMINE_USED; - { - LOCK(cs_wallet); - std::set trusted_parents; - for (const auto* pcoin : GetSpendableTXs()) { - const auto& wtx{*pcoin}; - - const bool is_trusted{IsTrusted(*&wtx, trusted_parents)}; - const int tx_depth{wtx.GetDepthInMainChain()}; - const CAmount tx_credit_mine{wtx.GetAvailableCredit(/* fUseCache */ true, ISMINE_SPENDABLE | reuse_filter)}; - const CAmount tx_credit_watchonly{wtx.GetAvailableCredit(/* fUseCache */ true, ISMINE_WATCH_ONLY | reuse_filter)}; - if (is_trusted && ((tx_depth >= min_depth) || (fAddLocked && wtx.IsLockedByInstantSend()))) { - ret.m_mine_trusted += tx_credit_mine; - ret.m_watchonly_trusted += tx_credit_watchonly; - } - if (!is_trusted && tx_depth == 0 && wtx.InMempool()) { - ret.m_mine_untrusted_pending += tx_credit_mine; - ret.m_watchonly_untrusted_pending += tx_credit_watchonly; - } - ret.m_mine_immature += wtx.GetImmatureCredit(); - ret.m_watchonly_immature += wtx.GetImmatureWatchOnlyCredit(); - if (CCoinJoinClientOptions::IsEnabled()) { - ret.m_anonymized += wtx.GetAnonymizedCredit(coinControl); - ret.m_denominated_trusted += wtx.GetDenominatedCredit(false); - ret.m_denominated_untrusted_pending += wtx.GetDenominatedCredit(true); - } - } - } - return ret; -} - CAmount CWallet::GetAnonymizableBalance(bool fSkipDenominated, bool fSkipUnconfirmed) const { if (!CCoinJoinClientOptions::IsEnabled()) return 0; @@ -2642,457 +2239,77 @@ CAmount CWallet::GetNormalizedAnonymizedBalance() const return nTotal; } -CAmount CWallet::GetAvailableBalance(const CCoinControl* coinControl) const +const uint256& CWallet::GetCoinJoinSalt() { - LOCK(cs_wallet); - - CAmount balance = 0; - std::vector vCoins; - AvailableCoins(vCoins, coinControl); - for (const COutput& out : vCoins) { - if (out.fSpendable) { - balance += out.tx->tx->vout[out.i].nValue; - } + if (nCoinJoinSalt.IsNull()) { + InitCJSaltFromDb(); } - return balance; + return nCoinJoinSalt; } -void CWallet::AvailableCoins(std::vector &vCoins, const CCoinControl* coinControl, const CAmount& nMinimumAmount, const CAmount& nMaximumAmount, const CAmount &nMinimumSumAmount, const uint64_t nMaximumCount) const +void CWallet::InitCJSaltFromDb() { - AssertLockHeld(cs_wallet); + assert(nCoinJoinSalt.IsNull()); - vCoins.clear(); - CoinType nCoinType = coinControl ? coinControl->nCoinType : CoinType::ALL_COINS; + WalletBatch batch(GetDatabase()); + if (!batch.ReadCoinJoinSalt(nCoinJoinSalt) && batch.ReadCoinJoinSalt(nCoinJoinSalt, true)) { + // Migrate salt stored with legacy key + batch.WriteCoinJoinSalt(nCoinJoinSalt); + } +} - CAmount nTotal = 0; - // Either the WALLET_FLAG_AVOID_REUSE flag is not set (in which case we always allow), or we default to avoiding, and only in the case where - // a coin control object is provided, and has the avoid address reuse flag set to false, do we allow already used addresses - bool allow_used_addresses = !IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE) || (coinControl && !coinControl->m_avoid_address_reuse); - const int min_depth = {coinControl ? coinControl->m_min_depth : DEFAULT_MIN_DEPTH}; - const int max_depth = {coinControl ? coinControl->m_max_depth : DEFAULT_MAX_DEPTH}; - const bool only_safe = {coinControl ? !coinControl->m_include_unsafe_inputs : true}; +bool CWallet::SetCoinJoinSalt(const uint256& cj_salt) +{ + WalletBatch batch(GetDatabase()); + // Only store new salt in CWallet if database write is successful + if (batch.WriteCoinJoinSalt(cj_salt)) { + nCoinJoinSalt = cj_salt; + return true; + } + return false; +} - std::set trusted_parents; - for (const auto* pcoin : GetSpendableTXs()) { - const auto& wtx{*pcoin}; +struct CompareByPriority +{ + bool operator()(const COutput& t1, + const COutput& t2) const + { + return CoinJoin::CalculateAmountPriority(t1.GetInputCoin().effective_value) > CoinJoin::CalculateAmountPriority(t2.GetInputCoin().effective_value); + } +}; - const uint256& wtxid = wtx.GetHash(); +bool CWallet::SignTransaction(CMutableTransaction& tx) const +{ + AssertLockHeld(cs_wallet); - if (!chain().checkFinalTx(*wtx.tx)) - continue; + // Build coins map + std::map coins; + for (auto& input : tx.vin) { + std::map::const_iterator mi = mapWallet.find(input.prevout.hash); + if(mi == mapWallet.end() || input.prevout.n >= mi->second.tx->vout.size()) { + return false; + } + const CWalletTx& wtx = mi->second; + coins[input.prevout] = Coin(wtx.tx->vout[input.prevout.n], wtx.m_confirm.block_height, wtx.IsCoinBase()); + } + std::map input_errors; + return SignTransaction(tx, coins, SIGHASH_ALL, input_errors); +} - if (wtx.IsImmatureCoinBase()) - continue; +bool CWallet::SignTransaction(CMutableTransaction& tx, const std::map& coins, int sighash, std::map& input_errors) const +{ + // Try to sign with all ScriptPubKeyMans + for (ScriptPubKeyMan* spk_man : GetAllScriptPubKeyMans()) { + // spk_man->SignTransaction will return true if the transaction is complete, + // so we can exit early and return true if that happens + if (spk_man->SignTransaction(tx, coins, sighash, input_errors)) { + return true; + } + } - int nDepth = wtx.GetDepthInMainChain(); - - // We should not consider coins which aren't at least in our mempool - // It's possible for these to be conflicted via ancestors which we may never be able to detect - if (nDepth == 0 && !wtx.InMempool()) - continue; - - bool safeTx = IsTrusted(*&wtx, trusted_parents); - - if (only_safe && !safeTx) { - continue; - } - - if (nDepth < min_depth || nDepth > max_depth) { - continue; - } - - for (unsigned int i = 0; i < wtx.tx->vout.size(); i++) { - bool found = false; - switch (nCoinType) { - case CoinType::ONLY_FULLY_MIXED: { - found = CoinJoin::IsDenominatedAmount(wtx.tx->vout[i].nValue) && - IsFullyMixed(COutPoint(wtxid, i)); - break; - } - case CoinType::ONLY_READY_TO_MIX: { - found = CoinJoin::IsDenominatedAmount(wtx.tx->vout[i].nValue) && - !IsFullyMixed(COutPoint(wtxid, i)); - break; - } - case CoinType::ONLY_NONDENOMINATED: { - // NOTE: do not use collateral amounts - found = !CoinJoin::IsCollateralAmount(wtx.tx->vout[i].nValue) && - !CoinJoin::IsDenominatedAmount(wtx.tx->vout[i].nValue); - break; - } - case CoinType::ONLY_MASTERNODE_COLLATERAL: { - found = dmn_types::IsCollateralAmount(wtx.tx->vout[i].nValue); - break; - } - case CoinType::ONLY_COINJOIN_COLLATERAL: { - found = CoinJoin::IsCollateralAmount(wtx.tx->vout[i].nValue); - break; - } - case CoinType::ALL_COINS: { - found = true; - break; - } - } // no default case, so the compiler can warn about missing cases - if(!found) continue; - - // Only consider selected coins if add_inputs is false - if (coinControl && !coinControl->m_add_inputs && !coinControl->IsSelected(COutPoint(wtxid, i))) { - continue; - } - - if (wtx.tx->vout[i].nValue < nMinimumAmount || wtx.tx->vout[i].nValue > nMaximumAmount) - continue; - - if (coinControl && coinControl->HasSelected() && !coinControl->fAllowOtherInputs && !coinControl->IsSelected(COutPoint(wtxid, i))) - continue; - - if (IsLockedCoin(wtxid, i) && nCoinType != CoinType::ONLY_MASTERNODE_COLLATERAL) - continue; - - if (IsSpent(wtxid, i)) - continue; - - isminetype mine = IsMine(wtx.tx->vout[i]); - - if (mine == ISMINE_NO) { - continue; - } - - if (!allow_used_addresses && IsSpentKey(wtxid, i)) { - continue; - } - - std::unique_ptr provider = GetSolvingProvider(wtx.tx->vout[i].scriptPubKey); - - bool solvable = provider ? IsSolvable(*provider, wtx.tx->vout[i].scriptPubKey) : false; - bool spendable = ((mine & ISMINE_SPENDABLE) != ISMINE_NO) || (((mine & ISMINE_WATCH_ONLY) != ISMINE_NO) && (coinControl && coinControl->fAllowWatchOnly && solvable)); - - vCoins.push_back(COutput(&wtx, i, nDepth, spendable, solvable, safeTx, (coinControl && coinControl->fAllowWatchOnly))); - - // Checks the sum amount of all UTXO's. - if (nMinimumSumAmount != MAX_MONEY) { - nTotal += wtx.tx->vout[i].nValue; - - if (nTotal >= nMinimumSumAmount) { - return; - } - } - - // Checks the maximum number of UTXO's. - if (nMaximumCount > 0 && vCoins.size() >= nMaximumCount) { - return; - } - } - } -} - -std::map> CWallet::ListCoins() const -{ - AssertLockHeld(cs_wallet); - - std::map> result; - std::vector availableCoins; - - AvailableCoins(availableCoins); - - for (const COutput& coin : availableCoins) { - CTxDestination address; - if ((coin.fSpendable || (IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && coin.fSolvable)) && - ExtractDestination(FindNonChangeParentOutput(*coin.tx->tx, coin.i).scriptPubKey, address)) { - result[address].emplace_back(std::move(coin)); - } - } - - std::vector lockedCoins; - ListLockedCoins(lockedCoins); - // Include watch-only for LegacyScriptPubKeyMan wallets without private keys - const bool include_watch_only = GetLegacyScriptPubKeyMan() && IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS); - const isminetype is_mine_filter = include_watch_only ? ISMINE_WATCH_ONLY : ISMINE_SPENDABLE; - for (const COutPoint& output : lockedCoins) { - auto it = mapWallet.find(output.hash); - if (it != mapWallet.end()) { - int depth = it->second.GetDepthInMainChain(); - if (depth >= 0 && output.n < it->second.tx->vout.size() && - IsMine(it->second.tx->vout[output.n]) == is_mine_filter - ) { - CTxDestination address; - if (ExtractDestination(FindNonChangeParentOutput(*it->second.tx, output.n).scriptPubKey, address)) { - result[address].emplace_back( - &it->second, output.n, depth, true /* spendable */, true /* solvable */, false /* safe */); - } - } - } - } - - return result; -} - -const CTxOut& CWallet::FindNonChangeParentOutput(const CTransaction& tx, int output) const -{ - AssertLockHeld(cs_wallet); - const CTransaction* ptx = &tx; - int n = output; - while (IsChange(ptx->vout[n]) && ptx->vin.size() > 0) { - const COutPoint& prevout = ptx->vin[0].prevout; - auto it = mapWallet.find(prevout.hash); - if (it == mapWallet.end() || it->second.tx->vout.size() <= prevout.n || - !IsMine(it->second.tx->vout[prevout.n])) { - break; - } - ptx = it->second.tx.get(); - n = prevout.n; - } - return ptx->vout[n]; -} - -const uint256& CWallet::GetCoinJoinSalt() -{ - if (nCoinJoinSalt.IsNull()) { - InitCJSaltFromDb(); - } - return nCoinJoinSalt; -} - -void CWallet::InitCJSaltFromDb() -{ - assert(nCoinJoinSalt.IsNull()); - - WalletBatch batch(GetDatabase()); - if (!batch.ReadCoinJoinSalt(nCoinJoinSalt) && batch.ReadCoinJoinSalt(nCoinJoinSalt, true)) { - // Migrate salt stored with legacy key - batch.WriteCoinJoinSalt(nCoinJoinSalt); - } -} - -bool CWallet::SetCoinJoinSalt(const uint256& cj_salt) -{ - WalletBatch batch(GetDatabase()); - // Only store new salt in CWallet if database write is successful - if (batch.WriteCoinJoinSalt(cj_salt)) { - nCoinJoinSalt = cj_salt; - return true; - } - return false; -} - -struct CompareByPriority -{ - bool operator()(const COutput& t1, - const COutput& t2) const - { - return CoinJoin::CalculateAmountPriority(t1.GetInputCoin().effective_value) > CoinJoin::CalculateAmountPriority(t2.GetInputCoin().effective_value); - } -}; - -static bool isGroupISLocked(const OutputGroup& group, interfaces::Chain& chain) -{ - return std::all_of(group.m_outputs.begin(), group.m_outputs.end(), [&chain](const auto& output) { - return chain.isInstantSendLockedTx(output.outpoint.hash); - }); -} - -bool CWallet::SelectCoinsMinConf(const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, std::vector coins, - std::set& setCoinsRet, CAmount& nValueRet, const CoinSelectionParams& coin_selection_params, CoinType nCoinType) const -{ - setCoinsRet.clear(); - nValueRet = 0; - - // Note that unlike KnapsackSolver, we do not include the fee for creating a change output as BnB will not create a change output. - std::vector positive_groups = GroupOutputs(coins, coin_selection_params, eligibility_filter, true /* positive_only */); - if (SelectCoinsBnB(positive_groups, nTargetValue, coin_selection_params.m_cost_of_change, setCoinsRet, nValueRet)) { - return true; - } - // The knapsack solver has some legacy behavior where it will spend dust outputs. We retain this behavior, so don't filter for positive only here. - std::vector all_groups = GroupOutputs(coins, coin_selection_params, eligibility_filter, false /* positive_only */); - // While nTargetValue includes the transaction fees for non-input things, it does not include the fee for creating a change output. - // So we need to include that for KnapsackSolver as well, as we are expecting to create a change output. - return KnapsackSolver(nTargetValue + coin_selection_params.m_change_fee, all_groups, setCoinsRet, nValueRet, nCoinType == CoinType::ONLY_FULLY_MIXED, m_default_max_tx_fee); -} - -bool CWallet::SelectCoins(const std::vector& vAvailableCoins, const CAmount& nTargetValue, std::set& setCoinsRet, CAmount& nValueRet, const CCoinControl& coin_control, CoinSelectionParams& coin_selection_params) const -{ - // Note: this function should never be used for "always free" tx types like dstx - - std::vector vCoins(vAvailableCoins); - CoinType nCoinType = coin_control.nCoinType; - CAmount value_to_select = nTargetValue; - - // coin control -> return all selected outputs (we want all selected to go into the transaction for sure) - if (coin_control.HasSelected() && !coin_control.fAllowOtherInputs) - { - for (const COutput& out : vCoins) - { - if(!out.fSpendable) - continue; - - nValueRet += out.tx->tx->vout[out.i].nValue; - setCoinsRet.insert(out.GetInputCoin()); - - if (!coin_control.fRequireAllInputs && nValueRet >= nTargetValue) { - // stop when we added at least one input and enough inputs to have at least nTargetValue funds - return true; - } - } - - return (nValueRet >= nTargetValue); - } - - // calculate value from preset inputs and store them - std::set setPresetCoins; - CAmount nValueFromPresetInputs = 0; - - std::vector vPresetInputs; - coin_control.ListSelected(vPresetInputs); - for (const COutPoint& outpoint : vPresetInputs) - { - std::map::const_iterator it = mapWallet.find(outpoint.hash); - if (it != mapWallet.end()) - { - const CWalletTx& wtx = it->second; - // Clearly invalid input, fail - if (wtx.tx->vout.size() <= outpoint.n) { - return false; - } - if (nCoinType == CoinType::ONLY_FULLY_MIXED) { - // Make sure to include mixed preset inputs only, - // even if some non-mixed inputs were manually selected via CoinControl - if (!IsFullyMixed(outpoint)) continue; - } - // Just to calculate the marginal byte size - CInputCoin coin(wtx.tx, outpoint.n, wtx.GetSpendSize(outpoint.n, false)); - nValueFromPresetInputs += coin.txout.nValue; - if (coin.m_input_bytes <= 0) { - return false; // Not solvable, can't estimate size for fee - } - coin.effective_value = coin.txout.nValue - coin_selection_params.m_effective_feerate.GetFee(coin.m_input_bytes); - if (coin_selection_params.m_subtract_fee_outputs) { - value_to_select -= coin.txout.nValue; - } else { - value_to_select -= coin.effective_value; - } - setPresetCoins.insert(coin); - } else { - return false; // TODO: Allow non-wallet inputs - } - } - - // remove preset inputs from vCoins so that Coin Selection doesn't pick them. - for (std::vector::iterator it = vCoins.begin(); it != vCoins.end() && coin_control.HasSelected();) - { - if (setPresetCoins.count(it->GetInputCoin())) - it = vCoins.erase(it); - else - ++it; - } - - unsigned int limit_ancestor_count = 0; - unsigned int limit_descendant_count = 0; - chain().getPackageLimits(limit_ancestor_count, limit_descendant_count); - const size_t max_ancestors = (size_t)std::max(1, limit_ancestor_count); - const size_t max_descendants = (size_t)std::max(1, limit_descendant_count); - const bool fRejectLongChains = gArgs.GetBoolArg("-walletrejectlongchains", DEFAULT_WALLET_REJECT_LONG_CHAINS); - - // form groups from remaining coins; note that preset coins will not - // automatically have their associated (same address) coins included - if (coin_control.m_avoid_partial_spends && vCoins.size() > OUTPUT_GROUP_MAX_ENTRIES) { - // Cases where we have 101+ outputs all pointing to the same destination may result in - // privacy leaks as they will potentially be deterministically sorted. We solve that by - // explicitly shuffling the outputs before processing - Shuffle(vCoins.begin(), vCoins.end(), FastRandomContext()); - } - // Coin Selection attempts to select inputs from a pool of eligible UTXOs to fund the - // transaction at a target feerate. If an attempt fails, more attempts may be made using a more - // permissive CoinEligibilityFilter. - const bool res = [&] { - // Pre-selected inputs already cover the target amount. - if (value_to_select <= 0) return true; - - // If possible, fund the transaction with confirmed UTXOs only. Prefer at least six - // confirmations on outputs received from other wallets and only spend confirmed change. - if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(1, 6, 0), vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) return true; - if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(1, 1, 0), vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) return true; - - // Fall back to using zero confirmation change (but with as few ancestors in the mempool as - // possible) if we cannot fund the transaction otherwise. - if (m_spend_zero_conf_change) { - if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, 2), vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) return true; - if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, std::min((size_t)4, max_ancestors/3), std::min((size_t)4, max_descendants/3)), - vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) { - return true; - } - if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, max_ancestors/2, max_descendants/2), - vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) { - return true; - } - // If partial groups are allowed, relax the requirement of spending OutputGroups (groups - // of UTXOs sent to the same address, which are obviously controlled by a single wallet) - // in their entirety. - if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, max_ancestors-1, max_descendants-1, true /* include_partial_groups */), - vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) { - return true; - } - // Try with unsafe inputs if they are allowed. This may spend unconfirmed outputs - // received from other wallets. - if (coin_control.m_include_unsafe_inputs - && SelectCoinsMinConf(value_to_select, - CoinEligibilityFilter(0 /* conf_mine */, 0 /* conf_theirs */, max_ancestors-1, max_descendants-1, true /* include_partial_groups */), - vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) { - return true; - } - // Try with unlimited ancestors/descendants. The transaction will still need to meet - // mempool ancestor/descendant policy to be accepted to mempool and broadcasted, but - // OutputGroups use heuristics that may overestimate ancestor/descendant counts. - if (!fRejectLongChains && SelectCoinsMinConf(value_to_select, - CoinEligibilityFilter(0, 1, std::numeric_limits::max(), std::numeric_limits::max(), true /* include_partial_groups */), - vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) { - return true; - } - } - // Coin Selection failed. - return false; - }(); - - // SelectCoinsMinConf clears setCoinsRet, so add the preset inputs from coin_control to the coinset - util::insert(setCoinsRet, setPresetCoins); - - // add preset inputs to the total value selected - nValueRet += nValueFromPresetInputs; - - return res; -} - -bool CWallet::SignTransaction(CMutableTransaction& tx) const -{ - AssertLockHeld(cs_wallet); - - // Build coins map - std::map coins; - for (auto& input : tx.vin) { - std::map::const_iterator mi = mapWallet.find(input.prevout.hash); - if(mi == mapWallet.end() || input.prevout.n >= mi->second.tx->vout.size()) { - return false; - } - const CWalletTx& wtx = mi->second; - coins[input.prevout] = Coin(wtx.tx->vout[input.prevout.n], wtx.m_confirm.block_height, wtx.IsCoinBase()); - } - std::map input_errors; - return SignTransaction(tx, coins, SIGHASH_ALL, input_errors); -} - -bool CWallet::SignTransaction(CMutableTransaction& tx, const std::map& coins, int sighash, std::map& input_errors) const -{ - // Try to sign with all ScriptPubKeyMans - for (ScriptPubKeyMan* spk_man : GetAllScriptPubKeyMans()) { - // spk_man->SignTransaction will return true if the transaction is complete, - // so we can exit early and return true if that happens - if (spk_man->SignTransaction(tx, coins, sighash, input_errors)) { - return true; - } - } - - // At this point, one input was not fully signed otherwise we would have exited already - return false; -} + // At this point, one input was not fully signed otherwise we would have exited already + return false; +} TransactionError CWallet::FillPSBT(PartiallySignedTransaction& psbtx, bool& complete, int sighash_type, bool sign, bool bip32derivs, size_t * n_signed, bool finalize) const { @@ -3171,64 +2388,6 @@ bool CWallet::SignSpecialTxPayload(const uint256& hash, const CKeyID& keyid, std return false; } -bool CWallet::FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, int& nChangePosInOut, bilingual_str& error, bool lockUnspents, const std::set& setSubtractFeeFromOutputs, CCoinControl coinControl) -{ - std::vector vecSend; - - // If no specific change position was requested, apply BIP69 - if (nChangePosInOut == -1) { - std::sort(tx.vin.begin(), tx.vin.end(), CompareInputBIP69()); - std::sort(tx.vout.begin(), tx.vout.end(), CompareOutputBIP69()); - } - - // Turn the txout set into a CRecipient vector. - for (size_t idx = 0; idx < tx.vout.size(); idx++) { - const CTxOut& txOut = tx.vout[idx]; - CRecipient recipient = {txOut.scriptPubKey, txOut.nValue, setSubtractFeeFromOutputs.count(idx) == 1}; - vecSend.push_back(recipient); - } - - coinControl.fAllowOtherInputs = true; - - for (const CTxIn& txin : tx.vin) { - coinControl.Select(txin.prevout); - } - - // Acquire the locks to prevent races to the new locked unspents between the - // CreateTransaction call and LockCoin calls (when lockUnspents is true). - LOCK(cs_wallet); - - CTransactionRef tx_new; - FeeCalculation fee_calc_out; - if (!CreateTransaction(vecSend, tx_new, nFeeRet, nChangePosInOut, error, coinControl, fee_calc_out, false, tx.vExtraPayload.size())) { - return false; - } - - if (nChangePosInOut != -1) { - tx.vout.insert(tx.vout.begin() + nChangePosInOut, tx_new->vout[nChangePosInOut]); - } - - // Copy output sizes from new transaction; they may have had the fee - // subtracted from them. - for (unsigned int idx = 0; idx < tx.vout.size(); idx++) { - tx.vout[idx].nValue = tx_new->vout[idx].nValue; - } - - // Add new txins while keeping original txin scriptSig/order. - for (const CTxIn& txin : tx_new->vin) { - if (!coinControl.IsSelected(txin.prevout)) { - tx.vin.push_back(txin); - - } - if (lockUnspents) { - LockCoin(txin.prevout); - } - - } - - return true; -} - bool CWallet::SelectTxDSInsByDenomination(int nDenom, CAmount nValueMax, std::vector& vecTxDSInRet) { LOCK(cs_wallet); @@ -3275,66 +2434,6 @@ bool CWallet::SelectTxDSInsByDenomination(int nDenom, CAmount nValueMax, std::ve return nValueTotal > 0; } -static bool IsCurrentForAntiFeeSniping(interfaces::Chain& chain, const uint256& block_hash) -{ - if (chain.isInitialBlockDownload()) { - return false; - } - constexpr int64_t MAX_ANTI_FEE_SNIPING_TIP_AGE = 8 * 60 * 60; // in seconds - int64_t block_time; - CHECK_NONFATAL(chain.findBlock(block_hash, FoundBlock().time(block_time))); - if (block_time < (GetTime() - MAX_ANTI_FEE_SNIPING_TIP_AGE)) { - return false; - } - return true; -} - -/** - * Return a height-based locktime for new transactions (uses the height of the - * current chain tip unless we are not synced with the current chain - */ -static uint32_t GetLocktimeForNewTransaction(interfaces::Chain& chain, const uint256& block_hash, int block_height) -{ - uint32_t locktime; - // Discourage fee sniping. - // - // For a large miner the value of the transactions in the best block and - // the mempool can exceed the cost of deliberately attempting to mine two - // blocks to orphan the current best block. By setting nLockTime such that - // only the next block can include the transaction, we discourage this - // practice as the height restricted and limited blocksize gives miners - // considering fee sniping fewer options for pulling off this attack. - // - // A simple way to think about this is from the wallet's point of view we - // always want the blockchain to move forward. By setting nLockTime this - // way we're basically making the statement that we only want this - // transaction to appear in the next block; we don't want to potentially - // encourage reorgs by allowing transactions to appear at lower heights - // than the next block in forks of the best chain. - // - // Of course, the subsidy is high enough, and transaction volume low - // enough, that fee sniping isn't a problem yet, but by implementing a fix - // now we ensure code won't be written that makes assumptions about - // nLockTime that preclude a fix later. - if (IsCurrentForAntiFeeSniping(chain, block_hash)) { - locktime = block_height; - - // Secondly occasionally randomly pick a nLockTime even further back, so - // that transactions that are delayed after signing for whatever reason, - // e.g. high-latency mix networks and some CoinJoin implementations, have - // better privacy. - if (GetRandInt(10) == 0) - locktime = std::max(0, (int)locktime - GetRandInt(100)); - } else { - // If our chain is lagging behind, we can't discourage fee sniping nor help - // the privacy of high-latency transactions. To avoid leaking a potentially - // unique "nLockTime fingerprint", set nLockTime to a constant. - locktime = 0; - } - assert(locktime < LOCKTIME_THRESHOLD); - return locktime; -} - std::vector CWallet::SelectCoinsGroupedByAddresses(bool fSkipDenominated, bool fAnonymizable, bool fSkipUnconfirmed, int nMaxOupointsPerAddress) const { LOCK(cs_wallet); @@ -3518,399 +2617,6 @@ bool CWallet::GetBudgetSystemCollateralTX(CTransactionRef& tx, uint256 hash, CAm return true; } -bool CWallet::CreateTransactionInternal( - const std::vector& vecSend, - CTransactionRef& tx, - CAmount& nFeeRet, - int& nChangePosInOut, - bilingual_str& error, - const CCoinControl& coin_control, - FeeCalculation& fee_calc_out, - bool sign, - int nExtraPayloadSize) -{ - CAmount nValue = 0; - ReserveDestination reservedest(this); - const bool sort_bip69{nChangePosInOut == -1}; - unsigned int nSubtractFeeFromAmount = 0; - for (const auto& recipient : vecSend) - { - if (nValue < 0 || recipient.nAmount < 0) - { - error = _("Transaction amounts must not be negative"); - return false; - } - nValue += recipient.nAmount; - - if (recipient.fSubtractFeeFromAmount) - nSubtractFeeFromAmount++; - } - if (vecSend.empty()) - { - error = _("Transaction must have at least one recipient"); - return false; - } - - CMutableTransaction txNew; - FeeCalculation feeCalc; - int nBytes{0}; - CAmount fee_needed{0}; - { - std::set setCoins; - LOCK(cs_wallet); - txNew.nLockTime = GetLocktimeForNewTransaction(chain(), GetLastBlockHash(), GetLastBlockHeight()); - { - std::vector vAvailableCoins; - AvailableCoins(vAvailableCoins, &coin_control, 1, MAX_MONEY, MAX_MONEY, 0); - CoinSelectionParams coin_selection_params; // Parameters for coin selection, init with dummy - coin_selection_params.m_avoid_partial_spends = coin_control.m_avoid_partial_spends; - - // Create change script that will be used if we need change - // TODO: pass in scriptChange instead of reservedest so - // change transaction isn't always pay-to-bitcoin-address - CScript scriptChange; - - // coin control: send change to custom address - if (!std::get_if(&coin_control.destChange)) { - scriptChange = GetScriptForDestination(coin_control.destChange); - } else { // no coin control: send change to newly generated address - // Note: We use a new key here to keep it from being obvious which side is the change. - // The drawback is that by not reusing a previous key, the change may be lost if a - // backup is restored, if the backup doesn't have the new private key for the change. - // If we reused the old key, it would be possible to add code to look for and - // rediscover unknown transactions that were written with keys of ours to recover - // post-backup change. - - // Reserve a new key pair from key pool. If it fails, provide a dummy - // destination in case we don't need change. - CTxDestination dest; - if (!reservedest.GetReservedDestination(dest, true)) { - error = _("Transaction needs a change address, but we can't generate it. Please call keypoolrefill first."); - } - scriptChange = GetScriptForDestination(dest); - // A valid destination implies a change script (and - // vice-versa). An empty change script will abort later, if the - // change keypool ran out, but change is required. - CHECK_NONFATAL(IsValidDestination(dest) != scriptChange.empty()); - } - CTxOut change_prototype_txout(0, scriptChange); - coin_selection_params.change_output_size = GetSerializeSize(change_prototype_txout); - - // Get size of spending the change output - int change_spend_size = CalculateMaximumSignedInputSize(change_prototype_txout, this); - // If the wallet doesn't know how to sign change output, assume p2sh-p2pkh - // as lower-bound to allow BnB to do it's thing - if (change_spend_size == -1) { - coin_selection_params.change_spend_size = DUMMY_NESTED_P2PKH_INPUT_SIZE; - } else { - coin_selection_params.change_spend_size = (size_t)change_spend_size; - } - - // Set discard feerate - coin_selection_params.m_discard_feerate = coin_control.m_discard_feerate ? *coin_control.m_discard_feerate : GetDiscardRate(*this); - - // Get the fee rate to use effective values in coin selection - coin_selection_params.m_effective_feerate = GetMinimumFeeRate(*this, coin_control, &feeCalc); - // Do not, ever, assume that it's fine to change the fee rate if the user has explicitly - // provided one - if (coin_control.m_feerate && coin_selection_params.m_effective_feerate > *coin_control.m_feerate) { - error = strprintf(_("Fee rate (%s) is lower than the minimum fee rate setting (%s)"), coin_control.m_feerate->ToString(FeeEstimateMode::DUFF_B), coin_selection_params.m_effective_feerate.ToString(FeeEstimateMode::DUFF_B)); - return false; - } - if (feeCalc.reason == FeeReason::FALLBACK && !m_allow_fallback_fee) { - // eventually allow a fallback fee - error = _("Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable -fallbackfee."); - return false; - } - - // Get long term estimate - CCoinControl cc_temp; - cc_temp.m_confirm_target = chain().estimateMaxBlocks(); - coin_selection_params.m_long_term_feerate = GetMinimumFeeRate(*this, cc_temp, nullptr); - - // Calculate the cost of change - // Cost of change is the cost of creating the change output + cost of spending the change output in the future. - // For creating the change output now, we use the effective feerate. - // For spending the change output in the future, we use the discard feerate for now. - // So cost of change = (change output size * effective feerate) + (size of spending change output * discard feerate) - coin_selection_params.m_change_fee = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.change_output_size); - coin_selection_params.m_cost_of_change = coin_selection_params.m_discard_feerate.GetFee(coin_selection_params.change_spend_size) + coin_selection_params.m_change_fee; - - coin_selection_params.m_subtract_fee_outputs = nSubtractFeeFromAmount != 0; // If we are doing subtract fee from recipient, don't use effective values - - // vouts to the payees - if (!coin_selection_params.m_subtract_fee_outputs) { - coin_selection_params.tx_noinputs_size = 9; // Static vsize overhead + outputs vsize. 4 nVersion, 4 nLocktime, 1 input count - coin_selection_params.tx_noinputs_size += GetSizeOfCompactSize(vecSend.size()); // bytes for output count - } - for (const auto& recipient : vecSend) - { - CTxOut txout(recipient.nAmount, recipient.scriptPubKey); - - // Include the fee cost for outputs. - if (!coin_selection_params.m_subtract_fee_outputs) { - coin_selection_params.tx_noinputs_size += ::GetSerializeSize(txout, PROTOCOL_VERSION); - } - - if (IsDust(txout, chain().relayDustFee())) - { - error = _("Transaction amount too small"); - return false; - } - txNew.vout.push_back(txout); - } - - // Include the fees for things that aren't inputs, excluding the change output - const CAmount not_input_fees = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.tx_noinputs_size); - CAmount nValueToSelect = nValue + not_input_fees; - - // Choose coins to use - CAmount inputs_sum = 0; - setCoins.clear(); - if (!SelectCoins(vAvailableCoins, /* nTargetValue */ nValueToSelect, setCoins, inputs_sum, coin_control, coin_selection_params)) { - if (coin_control.nCoinType == CoinType::ONLY_NONDENOMINATED) { - error = _("Unable to locate enough non-denominated funds for this transaction."); - } else if (coin_control.nCoinType == CoinType::ONLY_FULLY_MIXED) { - error = _("Unable to locate enough mixed funds for this transaction."); - error = error + Untranslated(" ") + strprintf(_("%s uses exact denominated amounts to send funds, you might simply need to mix some more coins."), gCoinJoinName); - } else { - error = _("Insufficient funds."); - } - return false; - } - - // Always make a change output - // We will reduce the fee from this change output later, and remove the output if it is too small. - const CAmount change_and_fee = inputs_sum - nValue; - assert(change_and_fee >= 0); - CTxOut newTxOut(change_and_fee, scriptChange); - - if (nChangePosInOut == -1) - { - // Insert change txn at random position: - nChangePosInOut = GetRandInt(txNew.vout.size()+1); - } - else if ((unsigned int)nChangePosInOut > txNew.vout.size()) - { - error = _("Transaction change output index out of range"); - return false; - } - - assert(nChangePosInOut != -1); - auto change_position = txNew.vout.insert(txNew.vout.begin() + nChangePosInOut, newTxOut); - - // We're making a copy of vecSend because it's const, sortedVecSend should be used - // in place of vecSend in all subsequent usage. - std::vector sortedVecSend{vecSend}; - if (sort_bip69) { - std::sort(txNew.vout.begin(), txNew.vout.end(), CompareOutputBIP69()); - // The output reduction loop uses vecSend to map to txNew.vout, we need to - // shuffle them both to ensure this mapping remains consistent - std::sort(sortedVecSend.begin(), sortedVecSend.end(), - [](const CRecipient& a, const CRecipient& b) { - return a.nAmount < b.nAmount || (a.nAmount == b.nAmount && a.scriptPubKey < b.scriptPubKey); - }); - - // If there was a change output added before, we must update its position now - if (const auto it = std::find(txNew.vout.begin(), txNew.vout.end(), newTxOut); it != txNew.vout.end()) { - change_position = it; - nChangePosInOut = std::distance(txNew.vout.begin(), change_position); - } - }; - - // Dummy fill vin for maximum size estimation - // - for (const auto& coin : setCoins) { - txNew.vin.push_back(CTxIn(coin.outpoint, CScript())); - } - - // Calculate the transaction fee - nBytes = CalculateMaximumSignedTxSize(CTransaction(txNew), this, coin_control.fAllowWatchOnly); - if (nBytes < 0) { - error = _("Signing transaction failed"); - return false; - } - - if (nExtraPayloadSize != 0) { - // account for extra payload in fee calculation - nBytes += GetSizeOfCompactSize(nExtraPayloadSize) + nExtraPayloadSize; - } - - fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes); - - if (nSubtractFeeFromAmount == 0) { - change_position->nValue -= fee_needed; - } - - // We want to drop the change to fees if: - // 1. The change output would be dust - // 2. The change is within the (almost) exact match window, i.e. it is less than or equal to the cost of the change output (cost_of_change) - // 3. We are working with fully mixed CoinJoin denominations - CAmount change_amount = change_position->nValue; - if (IsDust(*change_position, coin_selection_params.m_discard_feerate) || change_amount <= coin_selection_params.m_cost_of_change || coin_control.nCoinType == CoinType::ONLY_FULLY_MIXED) - { - nChangePosInOut = -1; - change_amount = 0; - txNew.vout.erase(change_position); - - nBytes = CalculateMaximumSignedTxSize(CTransaction(txNew), this, coin_control.fAllowWatchOnly); - fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes); - } - - nFeeRet = inputs_sum - nValue - change_amount; - - // Update nFeeRet in case fee_needed changed due to dropping the change output - if (fee_needed <= change_and_fee - change_amount) { - nFeeRet = change_and_fee - change_amount; - } - - // Reduce output values for subtractFeeFromAmount - if (nSubtractFeeFromAmount != 0) { - CAmount to_reduce = fee_needed + change_amount - change_and_fee; - int i = 0; - bool fFirst = true; - for (const auto& recipient : sortedVecSend) - { - if (i == nChangePosInOut) { - ++i; - } - CTxOut& txout = txNew.vout[i]; - - if (recipient.fSubtractFeeFromAmount) - { - txout.nValue -= to_reduce / nSubtractFeeFromAmount; // Subtract fee equally from each selected recipient - - if (fFirst) // first receiver pays the remainder not divisible by output count - { - fFirst = false; - txout.nValue -= to_reduce % nSubtractFeeFromAmount; - } - // Error if this output is reduced to be below dust - if (IsDust(txout, chain().relayDustFee())) { - if (txout.nValue < 0) { - error = _("The transaction amount is too small to pay the fee"); - } else { - error = _("The transaction amount is too small to send after the fee has been deducted"); - } - return false; - } - } - ++i; - } - nFeeRet = fee_needed; - } - - // Give up if change keypool ran out and change is required - if (scriptChange.empty() && nChangePosInOut != -1) { - return false; - } - } - - // Fill in final vin and shuffle/sort it - txNew.vin.clear(); - - // Note how the sequence number is set to non-maxint so that - // the nLockTime set above actually works. - const uint32_t nSequence = CTxIn::SEQUENCE_FINAL - 1; - for (const auto& coin : setCoins) { - txNew.vin.push_back(CTxIn(coin.outpoint, CScript(), nSequence)); - } - if (sort_bip69) { std::sort(txNew.vin.begin(), txNew.vin.end(), CompareInputBIP69()); } - else { Shuffle(txNew.vin.begin(), txNew.vin.end(), FastRandomContext()); } - - if (sign && !SignTransaction(txNew)) { - error = _("Signing transaction failed"); - return false; - } - - // Return the constructed transaction data. - tx = MakeTransactionRef(std::move(txNew)); - - // Limit size - if ((sign && ::GetSerializeSize(*tx, PROTOCOL_VERSION) > MAX_STANDARD_TX_SIZE) || - (!sign && static_cast(nBytes) > MAX_STANDARD_TX_SIZE)) { - error = _("Transaction too large"); - return false; - } - } - - if (fee_needed > nFeeRet) { - error = _("Fee needed > fee paid"); - return false; - } - - if (nFeeRet > m_default_max_tx_fee) { - error = TransactionErrorString(TransactionError::MAX_FEE_EXCEEDED); - return false; - } - - if (gArgs.GetBoolArg("-walletrejectlongchains", DEFAULT_WALLET_REJECT_LONG_CHAINS)) { - // Lastly, ensure this tx will pass the mempool's chain limits - if (!chain().checkChainLimits(tx)) { - error = _("Transaction has too long of a mempool chain"); - return false; - } - } - - // Before we return success, we assume any change key will be used to prevent - // accidental re-use. - reservedest.KeepDestination(); - fee_calc_out = feeCalc; - - WalletLogPrintf("Fee Calculation: Fee:%d Bytes:%u Tgt:%d (requested %d) Reason:\"%s\" Decay %.5f: Estimation: (%g - %g) %.2f%% %.1f/(%.1f %d mem %.1f out) Fail: (%g - %g) %.2f%% %.1f/(%.1f %d mem %.1f out)\n", - nFeeRet, nBytes, feeCalc.returnedTarget, feeCalc.desiredTarget, StringForFeeReason(feeCalc.reason), feeCalc.est.decay, - feeCalc.est.pass.start, feeCalc.est.pass.end, - (feeCalc.est.pass.totalConfirmed + feeCalc.est.pass.inMempool + feeCalc.est.pass.leftMempool) > 0.0 ? 100 * feeCalc.est.pass.withinTarget / (feeCalc.est.pass.totalConfirmed + feeCalc.est.pass.inMempool + feeCalc.est.pass.leftMempool) : 0.0, - feeCalc.est.pass.withinTarget, feeCalc.est.pass.totalConfirmed, feeCalc.est.pass.inMempool, feeCalc.est.pass.leftMempool, - feeCalc.est.fail.start, feeCalc.est.fail.end, - (feeCalc.est.fail.totalConfirmed + feeCalc.est.fail.inMempool + feeCalc.est.fail.leftMempool) > 0.0 ? 100 * feeCalc.est.fail.withinTarget / (feeCalc.est.fail.totalConfirmed + feeCalc.est.fail.inMempool + feeCalc.est.fail.leftMempool) : 0.0, - feeCalc.est.fail.withinTarget, feeCalc.est.fail.totalConfirmed, feeCalc.est.fail.inMempool, feeCalc.est.fail.leftMempool); - return true; -} - -bool CWallet::CreateTransaction( - const std::vector& vecSend, - CTransactionRef& tx, - CAmount& nFeeRet, - int& nChangePosInOut, - bilingual_str& error, - const CCoinControl& coin_control, - FeeCalculation& fee_calc_out, - bool sign, - int nExtraPayloadSize) -{ - int nChangePosIn = nChangePosInOut; - Assert(!tx); // tx is an out-param. TODO change the return type from bool to tx (or nullptr) - bool res = CreateTransactionInternal(vecSend, tx, nFeeRet, nChangePosInOut, error, coin_control, fee_calc_out, sign, nExtraPayloadSize); - // try with avoidpartialspends unless it's enabled already - if (res && nFeeRet > 0 /* 0 means non-functional fee rate estimation */ && m_max_aps_fee > -1 && !coin_control.m_avoid_partial_spends) { - CCoinControl tmp_cc = coin_control; - tmp_cc.m_avoid_partial_spends = true; - - // Re-use the change destination from the first creation attempt to avoid skipping BIP44 indexes - const int ungrouped_change_pos = nChangePosInOut; - if (ungrouped_change_pos != -1) { - ExtractDestination(tx->vout[ungrouped_change_pos].scriptPubKey, tmp_cc.destChange); - } - - CAmount nFeeRet2; - CTransactionRef tx2; - int nChangePosInOut2 = nChangePosIn; - bilingual_str error2; // fired and forgotten; if an error occurs, we discard the results - if (CreateTransactionInternal(vecSend, tx2, nFeeRet2, nChangePosInOut2, error2, tmp_cc, fee_calc_out, sign, nExtraPayloadSize)) { - // if fee of this alternative one is within the range of the max fee, we use this one - const bool use_aps = nFeeRet2 <= nFeeRet + m_max_aps_fee; - WalletLogPrintf("Fee non-grouped = %lld, grouped = %lld, using %s\n", nFeeRet, nFeeRet2, use_aps ? "grouped" : "non-grouped"); - if (use_aps) { - tx = tx2; - nFeeRet = nFeeRet2; - nChangePosInOut = nChangePosInOut2; - } - } - } - return res; -} - void CWallet::CommitTransaction(CTransactionRef tx, mapValue_t mapValue, std::vector> orderForm) { LOCK(cs_wallet); @@ -4210,138 +2916,6 @@ void CWallet::MarkDestinationsDirty(const std::set& destinations } } -std::map CWallet::GetAddressBalances() const -{ - std::map balances; - - { - LOCK(cs_wallet); - std::set trusted_parents; - for (const auto& walletEntry : mapWallet) - { - const CWalletTx& wtx = walletEntry.second; - - if (!IsTrusted(*&wtx, trusted_parents)) - continue; - - if (wtx.IsImmatureCoinBase()) - continue; - - int nDepth = wtx.GetDepthInMainChain(); - if ((nDepth < (wtx.IsFromMe(ISMINE_ALL) ? 0 : 1)) && !wtx.IsLockedByInstantSend()) - continue; - - for (unsigned int i = 0; i < wtx.tx->vout.size(); i++) - { - CTxDestination addr; - if (!IsMine(wtx.tx->vout[i])) - continue; - if(!ExtractDestination(wtx.tx->vout[i].scriptPubKey, addr)) - continue; - - CAmount n = IsSpent(walletEntry.first, i) ? 0 : wtx.tx->vout[i].nValue; - - balances[addr] += n; - } - } - } - - return balances; -} - -std::set< std::set > CWallet::GetAddressGroupings() const -{ - AssertLockHeld(cs_wallet); - std::set< std::set > groupings; - std::set grouping; - - for (const auto& walletEntry : mapWallet) - { - const CWalletTx& wtx = walletEntry.second; - - if (wtx.tx->vin.size() > 0) - { - bool any_mine = false; - // group all input addresses with each other - for (const CTxIn& txin : wtx.tx->vin) - { - CTxDestination address; - if(!IsMine(txin)) /* If this input isn't mine, ignore it */ - continue; - if(!ExtractDestination(mapWallet.at(txin.prevout.hash).tx->vout[txin.prevout.n].scriptPubKey, address)) - continue; - grouping.insert(address); - any_mine = true; - } - - // group change with input addresses - if (any_mine) - { - for (const CTxOut& txout : wtx.tx->vout) - if (IsChange(txout)) - { - CTxDestination txoutAddr; - if(!ExtractDestination(txout.scriptPubKey, txoutAddr)) - continue; - grouping.insert(txoutAddr); - } - } - if (grouping.size() > 0) - { - groupings.insert(grouping); - grouping.clear(); - } - } - - // group lone addrs by themselves - for (const auto& txout : wtx.tx->vout) - if (IsMine(txout)) - { - CTxDestination address; - if(!ExtractDestination(txout.scriptPubKey, address)) - continue; - grouping.insert(address); - groupings.insert(grouping); - grouping.clear(); - } - } - - std::set< std::set* > uniqueGroupings; // a set of pointers to groups of addresses - std::map< CTxDestination, std::set* > setmap; // map addresses to the unique group containing it - for (std::set _grouping : groupings) - { - // make a set of all the groups hit by this new group - std::set< std::set* > hits; - std::map< CTxDestination, std::set* >::iterator it; - for (const CTxDestination& address : _grouping) - if ((it = setmap.find(address)) != setmap.end()) - hits.insert((*it).second); - - // merge all hit groups into a new single group and delete old groups - std::set* merged = new std::set(_grouping); - for (std::set* hit : hits) - { - merged->insert(hit->begin(), hit->end()); - uniqueGroupings.erase(hit); - delete hit; - } - uniqueGroupings.insert(merged); - - // update setmap - for (const CTxDestination& element : *merged) - setmap[element] = merged; - } - - std::set< std::set > ret; - for (const std::set* uniqueGrouping : uniqueGroupings) - { - ret.insert(*uniqueGrouping); - delete uniqueGrouping; - } - - return ret; -} - std::set CWallet::GetLabelAddresses(const std::string& label) const { LOCK(cs_wallet); @@ -5470,94 +4044,6 @@ bool CWalletTx::IsImmatureCoinBase() const return GetBlocksToMaturity() > 0; } -std::vector CWallet::GroupOutputs(const std::vector& outputs, const CoinSelectionParams& coin_sel_params, const CoinEligibilityFilter& filter, bool positive_only) const -{ - std::vector groups_out; - - if (!coin_sel_params.m_avoid_partial_spends) { - // Allowing partial spends means no grouping. Each COutput gets its own OutputGroup. - for (const COutput& output : outputs) { - // Skip outputs we cannot spend - if (!output.fSpendable) continue; - - size_t ancestors, descendants; - chain().getTransactionAncestry(output.tx->GetHash(), ancestors, descendants); - CInputCoin input_coin = output.GetInputCoin(); - - // Make an OutputGroup containing just this output - OutputGroup group{coin_sel_params}; - group.Insert(input_coin, output.nDepth, output.tx->IsFromMe(ISMINE_ALL), ancestors, descendants, positive_only); - - // Check the OutputGroup's eligibility. Only add the eligible ones. - if (positive_only && group.GetSelectionAmount() <= 0) continue; - bool isISLocked = isGroupISLocked(group, chain()); - if (group.m_outputs.size() > 0 && group.EligibleForSpending(filter, isISLocked)) groups_out.push_back(group); - } - return groups_out; - } - - // We want to combine COutputs that have the same scriptPubKey into single OutputGroups - // except when there are more than OUTPUT_GROUP_MAX_ENTRIES COutputs grouped in an OutputGroup. - // To do this, we maintain a map where the key is the scriptPubKey and the value is a vector of OutputGroups. - // For each COutput, we check if the scriptPubKey is in the map, and if it is, the COutput's CInputCoin is added - // to the last OutputGroup in the vector for the scriptPubKey. When the last OutputGroup has - // OUTPUT_GROUP_MAX_ENTRIES CInputCoins, a new OutputGroup is added to the end of the vector. - std::map> spk_to_groups_map; - for (const auto& output : outputs) { - // Skip outputs we cannot spend - if (!output.fSpendable) continue; - - size_t ancestors, descendants; - chain().getTransactionAncestry(output.tx->GetHash(), ancestors, descendants); - CInputCoin input_coin = output.GetInputCoin(); - CScript spk = input_coin.txout.scriptPubKey; - - std::vector& groups = spk_to_groups_map[spk]; - - if (groups.size() == 0) { - // No OutputGroups for this scriptPubKey yet, add one - groups.emplace_back(coin_sel_params); - } - - // Get the last OutputGroup in the vector so that we can add the CInputCoin to it - // A pointer is used here so that group can be reassigned later if it is full. - OutputGroup* group = &groups.back(); - - // Check if this OutputGroup is full. We limit to OUTPUT_GROUP_MAX_ENTRIES when using -avoidpartialspends - // to avoid surprising users with very high fees. - if (group->m_outputs.size() >= OUTPUT_GROUP_MAX_ENTRIES) { - // The last output group is full, add a new group to the vector and use that group for the insertion - groups.emplace_back(coin_sel_params); - group = &groups.back(); - } - - // Add the input_coin to group - group->Insert(input_coin, output.nDepth, output.tx->IsFromMe(ISMINE_ALL), ancestors, descendants, positive_only); - } - - // Now we go through the entire map and pull out the OutputGroups - for (const auto& spk_and_groups_pair: spk_to_groups_map) { - const std::vector& groups_per_spk= spk_and_groups_pair.second; - - // Go through the vector backwards. This allows for the first item we deal with being the partial group. - for (auto group_it = groups_per_spk.rbegin(); group_it != groups_per_spk.rend(); group_it++) { - const OutputGroup& group = *group_it; - - // Don't include partial groups if there are full groups too and we don't want partial groups - if (group_it == groups_per_spk.rbegin() && groups_per_spk.size() > 1 && !filter.m_include_partial_groups) { - continue; - } - - // Check the OutputGroup's eligibility. Only add the eligible ones. - if (positive_only && group.GetSelectionAmount() <= 0) continue; - bool isISLocked = isGroupISLocked(group, chain()); - if (group.m_outputs.size() > 0 && group.EligibleForSpending(filter, isISLocked)) groups_out.push_back(group); - } - } - - return groups_out; -} - bool CWallet::IsCrypted() const { return HasEncryptionKeys(); diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index fe92535793943..bb5534ea5d2af 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -26,7 +26,10 @@ #include #include #include +#include #include +#include +#include #include #include @@ -241,374 +244,6 @@ struct CRecipient bool fSubtractFeeFromAmount; }; -typedef std::map mapValue_t; - - -static inline void ReadOrderPos(int64_t& nOrderPos, mapValue_t& mapValue) -{ - if (!mapValue.count("n")) - { - nOrderPos = -1; // TODO: calculate elsewhere - return; - } - nOrderPos = LocaleIndependentAtoi(mapValue["n"]); -} - - -static inline void WriteOrderPos(const int64_t& nOrderPos, mapValue_t& mapValue) -{ - if (nOrderPos == -1) - return; - mapValue["n"] = ToString(nOrderPos); -} - -struct COutputEntry -{ - CTxDestination destination; - CAmount amount; - int vout; -}; - -/** Legacy class used for deserializing vtxPrev for backwards compatibility. - * vtxPrev was removed in commit 93a18a3650292afbb441a47d1fa1b94aeb0164e3, - * but old wallet.dat files may still contain vtxPrev vectors of CMerkleTxs. - * These need to get deserialized for field alignment when deserializing - * a CWalletTx, but the deserialized values are discarded.**/ -class CMerkleTx -{ -public: - template - void Unserialize(Stream& s) - { - CTransactionRef tx; - uint256 hashBlock; - std::vector vMerkleBranch; - int nIndex; - - s >> tx >> hashBlock >> vMerkleBranch >> nIndex; - } -}; - -//Get the marginal bytes of spending the specified output -int CalculateMaximumSignedInputSize(const CTxOut& txout, const CWallet* pwallet, bool use_max_sig = false); - -/** - * A transaction with a bunch of additional info that only the owner cares about. - * It includes any unrecorded transactions needed to link it back to the block chain. - */ -class CWalletTx -{ -private: - const CWallet* const pwallet; - - /** Constant used in hashBlock to indicate tx has been abandoned, only used at - * serialization/deserialization to avoid ambiguity with conflicted. - */ - static constexpr const uint256& ABANDON_HASH = uint256::ONE; - - mutable bool fIsChainlocked{false}; - mutable bool fIsInstantSendLocked{false}; - -public: - /** - * Key/value map with information about the transaction. - * - * The following keys can be read and written through the map and are - * serialized in the wallet database: - * - * "comment", "to" - comment strings provided to sendtoaddress, - * and sendmany wallet RPCs - * "replaces_txid" - txid (as HexStr) of transaction replaced by - * bumpfee on transaction created by bumpfee - * "replaced_by_txid" - txid (as HexStr) of transaction created by - * bumpfee on transaction replaced by bumpfee - * "from", "message" - obsolete fields that could be set in UI prior to - * 2011 (removed in commit 4d9b223) - * - * The following keys are serialized in the wallet database, but shouldn't - * be read or written through the map (they will be temporarily added and - * removed from the map during serialization): - * - * "fromaccount" - serialized strFromAccount value - * "n" - serialized nOrderPos value - * "timesmart" - serialized nTimeSmart value - * "spent" - serialized vfSpent value that existed prior to - * 2014 (removed in commit 93a18a3) - */ - mapValue_t mapValue; - std::vector > vOrderForm; - unsigned int fTimeReceivedIsTxTime; - unsigned int nTimeReceived; //!< time received by this node - /** - * Stable timestamp that never changes, and reflects the order a transaction - * was added to the wallet. Timestamp is based on the block time for a - * transaction added as part of a block, or else the time when the - * transaction was received if it wasn't part of a block, with the timestamp - * adjusted in both cases so timestamp order matches the order transactions - * were added to the wallet. More details can be found in - * CWallet::ComputeTimeSmart(). - */ - unsigned int nTimeSmart; - /** - * From me flag is set to 1 for transactions that were created by the wallet - * on this bitcoin node, and set to 0 for transactions that were created - * externally and came in through the network or sendrawtransaction RPC. - */ - bool fFromMe; - int64_t nOrderPos; //!< position in ordered transaction list - std::multimap::const_iterator m_it_wtxOrdered; - - // memory only - enum AmountType { DEBIT, CREDIT, IMMATURE_CREDIT, AVAILABLE_CREDIT, ANON_CREDIT, DENOM_UCREDIT, DENOM_CREDIT, AMOUNTTYPE_ENUM_ELEMENTS }; - CAmount GetCachableAmount(AmountType type, const isminefilter& filter, bool recalculate = false) const; - mutable CachableAmount m_amounts[AMOUNTTYPE_ENUM_ELEMENTS]; - /** - * This flag is true if all m_amounts caches are empty. This is particularly - * useful in places where MarkDirty is conditionally called and the - * condition can be expensive and thus can be skipped if the flag is true. - * See MarkDestinationsDirty. - */ - mutable bool m_is_cache_empty{true}; - mutable bool fChangeCached; - mutable bool fInMempool; - mutable CAmount nChangeCached; - - CWalletTx(const CWallet* wallet, CTransactionRef arg) - : pwallet(wallet), - tx(std::move(arg)) - { - Init(); - } - - void Init() - { - mapValue.clear(); - vOrderForm.clear(); - fTimeReceivedIsTxTime = false; - nTimeReceived = 0; - nTimeSmart = 0; - fFromMe = false; - fChangeCached = false; - fInMempool = false; - nChangeCached = 0; - nOrderPos = -1; - m_confirm = Confirmation{}; - } - - CTransactionRef tx; - - /** New transactions start as UNCONFIRMED. At BlockConnected, - * they will transition to CONFIRMED. In case of reorg, at BlockDisconnected, - * they roll back to UNCONFIRMED. If we detect a conflicting transaction at - * block connection, we update conflicted tx and its dependencies as CONFLICTED. - * If tx isn't confirmed and outside of mempool, the user may switch it to ABANDONED - * by using the abandontransaction call. This last status may be override by a CONFLICTED - * or CONFIRMED transition. - */ - enum Status { - UNCONFIRMED, - CONFIRMED, - CONFLICTED, - ABANDONED - }; - - /** Confirmation includes tx status and a triplet of {block height/block hash/tx index in block} - * at which tx has been confirmed. All three are set to 0 if tx is unconfirmed or abandoned. - * Meaning of these fields changes with CONFLICTED state where they instead point to block hash - * and block height of the deepest conflicting tx. - */ - struct Confirmation { - Status status; - int block_height; - uint256 hashBlock; - int nIndex; - Confirmation(Status s = UNCONFIRMED, int b = 0, uint256 h = uint256(), int i = 0) : status(s), block_height(b), hashBlock(h), nIndex(i) {} - }; - - Confirmation m_confirm; - - template - void Serialize(Stream& s) const - { - mapValue_t mapValueCopy = mapValue; - - mapValueCopy["fromaccount"] = ""; - WriteOrderPos(nOrderPos, mapValueCopy); - if (nTimeSmart) { - mapValueCopy["timesmart"] = strprintf("%u", nTimeSmart); - } - - std::vector dummy_vector1; //!< Used to be vMerkleBranch - std::vector dummy_vector2; //!< Used to be vtxPrev - bool dummy_bool = false; //!< Used to be fSpent - uint256 serializedHash = isAbandoned() ? ABANDON_HASH : m_confirm.hashBlock; - int serializedIndex = isAbandoned() || isConflicted() ? -1 : m_confirm.nIndex; - s << tx << serializedHash << dummy_vector1 << serializedIndex << dummy_vector2 << mapValueCopy << vOrderForm << fTimeReceivedIsTxTime << nTimeReceived << fFromMe << dummy_bool; - } - - template - void Unserialize(Stream& s) - { - Init(); - - std::vector dummy_vector1; //!< Used to be vMerkleBranch - std::vector dummy_vector2; //!< Used to be vtxPrev - bool dummy_bool; //! Used to be fSpent - int serializedIndex; - s >> tx >> m_confirm.hashBlock >> dummy_vector1 >> serializedIndex >> dummy_vector2 >> mapValue >> vOrderForm >> fTimeReceivedIsTxTime >> nTimeReceived >> fFromMe >> dummy_bool; - - /* At serialization/deserialization, an nIndex == -1 means that hashBlock refers to - * the earliest block in the chain we know this or any in-wallet ancestor conflicts - * with. If nIndex == -1 and hashBlock is ABANDON_HASH, it means transaction is abandoned. - * In same context, an nIndex >= 0 refers to a confirmed transaction (if hashBlock set) or - * unconfirmed one. Older clients interpret nIndex == -1 as unconfirmed for backward - * compatibility (pre-commit 9ac63d6). - */ - if (serializedIndex == -1 && m_confirm.hashBlock == ABANDON_HASH) { - setAbandoned(); - } else if (serializedIndex == -1) { - setConflicted(); - } else if (!m_confirm.hashBlock.IsNull()) { - m_confirm.nIndex = serializedIndex; - setConfirmed(); - } - - ReadOrderPos(nOrderPos, mapValue); - nTimeSmart = mapValue.count("timesmart") ? (unsigned int)LocaleIndependentAtoi(mapValue["timesmart"]) : 0; - - mapValue.erase("fromaccount"); - mapValue.erase("spent"); - mapValue.erase("n"); - mapValue.erase("timesmart"); - } - - void SetTx(CTransactionRef arg) - { - tx = std::move(arg); - } - - //! make sure balances are recalculated - void MarkDirty() - { - m_amounts[DEBIT].Reset(); - m_amounts[CREDIT].Reset(); - m_amounts[ANON_CREDIT].Reset(); - m_amounts[DENOM_CREDIT].Reset(); - m_amounts[DENOM_UCREDIT].Reset(); - m_amounts[IMMATURE_CREDIT].Reset(); - m_amounts[AVAILABLE_CREDIT].Reset(); - fChangeCached = false; - m_is_cache_empty = true; - } - - const CWallet* GetWallet() const - { - return pwallet; - } - - //! filter decides which addresses will count towards the debit - CAmount GetDebit(const isminefilter& filter) const; - CAmount GetCredit(const isminefilter& filter) const; - CAmount GetImmatureCredit(bool fUseCache = true) const; - // TODO: Remove "NO_THREAD_SAFETY_ANALYSIS" and replace it with the correct - // annotation "EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet)". The - // annotation "NO_THREAD_SAFETY_ANALYSIS" was temporarily added to avoid - // having to resolve the issue of member access into incomplete type CWallet. - CAmount GetAvailableCredit(bool fUseCache = true, const isminefilter& filter = ISMINE_SPENDABLE) const NO_THREAD_SAFETY_ANALYSIS; - CAmount GetImmatureWatchOnlyCredit(const bool fUseCache = true) const; - CAmount GetChange() const; - - // TODO: Remove "NO_THREAD_SAFETY_ANALYSIS" and replace it with the correct - // annotation "EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet)". The - // annotation "NO_THREAD_SAFETY_ANALYSIS" was temporarily added to avoid - // having to resolve the issue of member access into incomplete type CWallet. - CAmount GetAnonymizedCredit(const CCoinControl* coinControl = nullptr) const NO_THREAD_SAFETY_ANALYSIS; - CAmount GetDenominatedCredit(bool unconfirmed, bool fUseCache=true) const NO_THREAD_SAFETY_ANALYSIS; - - /** Get the marginal bytes if spending the specified output from this transaction */ - int GetSpendSize(unsigned int out, bool use_max_sig = false) const - { - return CalculateMaximumSignedInputSize(tx->vout[out], pwallet, use_max_sig); - } - - void GetAmounts(std::list& listReceived, - std::list& listSent, CAmount& nFee, const isminefilter& filter) const; - - bool IsFromMe(const isminefilter& filter) const - { - return (GetDebit(filter) > 0); - } - - /** True if only scriptSigs are different */ - bool IsEquivalentTo(const CWalletTx& tx) const; - - bool InMempool() const; - bool IsTrusted() const; - - int64_t GetTxTime() const; - - bool CanBeResent() const; - - /** Pass this transaction to node for mempool insertion and relay to peers if flag set to true */ - bool SubmitMemoryPoolAndRelay(bilingual_str& err_string, bool relay); - - // TODO: Remove "NO_THREAD_SAFETY_ANALYSIS" and replace it with the correct - // annotation "EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet)". The annotation - // "NO_THREAD_SAFETY_ANALYSIS" was temporarily added to avoid having to - // resolve the issue of member access into incomplete type CWallet. Note - // that we still have the runtime check "AssertLockHeld(pwallet->cs_wallet)" - // in place. - std::set GetConflicts() const NO_THREAD_SAFETY_ANALYSIS; - - /** - * Return depth of transaction in blockchain: - * <0 : conflicts with a transaction this deep in the blockchain - * 0 : in memory pool, waiting to be included in a block - * >=1 : this many blocks deep in the main chain - */ - // TODO: Remove "NO_THREAD_SAFETY_ANALYSIS" and replace it with the correct - // annotation "EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet)". The annotation - // "NO_THREAD_SAFETY_ANALYSIS" was temporarily added to avoid having to - // resolve the issue of member access into incomplete type CWallet. Note - // that we still have the runtime check "AssertLockHeld(pwallet->cs_wallet)" - // in place. - int GetDepthInMainChain() const NO_THREAD_SAFETY_ANALYSIS; - bool IsInMainChain() const { return GetDepthInMainChain() > 0; } - bool IsLockedByInstantSend() const; - bool IsChainLocked() const NO_THREAD_SAFETY_ANALYSIS; - - /** - * @return number of blocks to maturity for this transaction: - * 0 : is not a coinbase transaction, or is a mature coinbase transaction - * >0 : is a coinbase transaction which matures in this many blocks - */ - int GetBlocksToMaturity() const; - bool isAbandoned() const { return m_confirm.status == CWalletTx::ABANDONED; } - void setAbandoned() - { - m_confirm.status = CWalletTx::ABANDONED; - m_confirm.hashBlock = uint256(); - m_confirm.block_height = 0; - m_confirm.nIndex = 0; - } - bool isConflicted() const { return m_confirm.status == CWalletTx::CONFLICTED; } - void setConflicted() { m_confirm.status = CWalletTx::CONFLICTED; } - bool isUnconfirmed() const { return m_confirm.status == CWalletTx::UNCONFIRMED; } - void setUnconfirmed() { m_confirm.status = CWalletTx::UNCONFIRMED; } - bool isConfirmed() const { return m_confirm.status == CWalletTx::CONFIRMED; } - void setConfirmed() { m_confirm.status = CWalletTx::CONFIRMED; } - const uint256& GetHash() const { return tx->GetHash(); } - bool IsCoinBase() const { return tx->IsCoinBase(); } - bool IsPlatformTransfer() const { return tx->IsPlatformTransfer(); } - bool IsImmatureCoinBase() const; - - // Disable copying of CWalletTx objects to prevent bugs where instances get - // copied in and out of the mapWallet map, and fields are updated in the - // wrong copy. - CWalletTx(CWalletTx const &) = delete; - void operator=(CWalletTx const &x) = delete; -}; - struct WalletTxHasher { StaticSaltedHasher h; @@ -618,58 +253,6 @@ struct WalletTxHasher } }; -class COutput -{ -public: - const CWalletTx *tx; - - /** Index in tx->vout. */ - int i; - - /** - * Depth in block chain. - * If > 0: the tx is on chain and has this many confirmations. - * If = 0: the tx is waiting confirmation. - * If < 0: a conflicting tx is on chain and has this many confirmations. */ - int nDepth; - - /** 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 nInputBytes; - - /** Whether we have the private keys to spend this output */ - bool fSpendable; - - /** Whether we know how to spend this output, ignoring the lack of keys */ - bool fSolvable; - - /** Whether to use the maximum sized, 72 byte signature when calculating the size of the input spend. This should only be set when watch-only outputs are allowed */ - bool use_max_sig; - - /** - * Whether this output is considered safe to spend. Unconfirmed transactions - * from outside keys and unconfirmed replacement transactions are considered - * unsafe and will not be used to fund new spending transactions. - */ - bool fSafe; - - COutput(const CWalletTx *txIn, int iIn, int nDepthIn, bool fSpendableIn, bool fSolvableIn, bool fSafeIn, bool use_max_sig_in = false) - { - tx = txIn; i = iIn; nDepth = nDepthIn; fSpendable = fSpendableIn; fSolvable = fSolvableIn; fSafe = fSafeIn; nInputBytes = -1; use_max_sig = use_max_sig_in; - // If known and signable by the given wallet, compute nInputBytes - // Failure will keep this value -1 - if (fSpendable && tx) { - nInputBytes = tx->GetSpendSize(i, use_max_sig); - } - } - - std::string ToString() const; - - inline CInputCoin GetInputCoin() const - { - return CInputCoin(tx->tx, i, nInputBytes); - } -}; - class WalletRescanReserver; //forward declarations for ScanForWalletTransactions/RescanFromTime /** * A CWallet maintains a set of transactions and balances, and provides the ability to create new transactions. diff --git a/test/lint/lint-circular-dependencies.py b/test/lint/lint-circular-dependencies.py index 7b5cc55ce89a5..1e3f93ec7b02a 100755 --- a/test/lint/lint-circular-dependencies.py +++ b/test/lint/lint-circular-dependencies.py @@ -23,6 +23,10 @@ "wallet/fees -> wallet/wallet -> wallet/fees", "wallet/wallet -> wallet/walletdb -> wallet/wallet", "node/coinstats -> validation -> node/coinstats", + # Temporary circular dependencies that allow wallet.h/wallet.cpp to be + # split up in a MOVEONLY commit. These are removed in #21206. + "wallet/receive -> wallet/wallet -> wallet/receive", + "wallet/spend -> wallet/wallet -> wallet/spend", # Dash "banman -> common/bloom -> evo/assetlocktx -> llmq/quorums -> net -> banman", "banman -> common/bloom -> evo/assetlocktx -> llmq/signing -> net_processing -> banman", From 38534991206ee807f06dbfaf02740fba820b5953 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 5 Jan 2025 15:50:48 +0000 Subject: [PATCH 03/16] merge bitcoin#22008: Cleanup and refactor CreateTransactionInternal --- src/bench/coin_selection.cpp | 2 +- src/wallet/spend.cpp | 543 ++++++++++++------------- src/wallet/test/coinselector_tests.cpp | 68 ++-- src/wallet/wallet.h | 4 +- 4 files changed, 304 insertions(+), 313 deletions(-) diff --git a/src/bench/coin_selection.cpp b/src/bench/coin_selection.cpp index da29c8bddbb92..6fd1ed6108ed1 100644 --- a/src/bench/coin_selection.cpp +++ b/src/bench/coin_selection.cpp @@ -55,7 +55,7 @@ static void CoinSelection(benchmark::Bench& bench) bench.run([&] { std::set setCoinsRet; CAmount nValueRet; - bool success = wallet.SelectCoinsMinConf(1003 * COIN, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params); + bool success = wallet.AttemptSelection(1003 * COIN, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params); assert(success); assert(nValueRet == 1003 * COIN); assert(setCoinsRet.size() == 2); diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp index 422f7e236ee28..6901caaa1179a 100644 --- a/src/wallet/spend.cpp +++ b/src/wallet/spend.cpp @@ -361,7 +361,7 @@ std::vector CWallet::GroupOutputs(const std::vector& outpu return groups_out; } -bool CWallet::SelectCoinsMinConf(const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, std::vector coins, +bool CWallet::AttemptSelection(const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, std::vector coins, std::set& setCoinsRet, CAmount& nValueRet, const CoinSelectionParams& coin_selection_params, CoinType nCoinType) const { setCoinsRet.clear(); @@ -479,32 +479,32 @@ bool CWallet::SelectCoins(const std::vector& vAvailableCoins, const CAm // If possible, fund the transaction with confirmed UTXOs only. Prefer at least six // confirmations on outputs received from other wallets and only spend confirmed change. - if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(1, 6, 0), vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) return true; - if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(1, 1, 0), vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) return true; + if (AttemptSelection(value_to_select, CoinEligibilityFilter(1, 6, 0), vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) return true; + if (AttemptSelection(value_to_select, CoinEligibilityFilter(1, 1, 0), vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) return true; // Fall back to using zero confirmation change (but with as few ancestors in the mempool as // possible) if we cannot fund the transaction otherwise. if (m_spend_zero_conf_change) { - if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, 2), vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) return true; - if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, std::min((size_t)4, max_ancestors/3), std::min((size_t)4, max_descendants/3)), + if (AttemptSelection(value_to_select, CoinEligibilityFilter(0, 1, 2), vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) return true; + if (AttemptSelection(value_to_select, CoinEligibilityFilter(0, 1, std::min((size_t)4, max_ancestors/3), std::min((size_t)4, max_descendants/3)), vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) { return true; } - if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, max_ancestors/2, max_descendants/2), + if (AttemptSelection(value_to_select, CoinEligibilityFilter(0, 1, max_ancestors/2, max_descendants/2), vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) { return true; } // If partial groups are allowed, relax the requirement of spending OutputGroups (groups // of UTXOs sent to the same address, which are obviously controlled by a single wallet) // in their entirety. - if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, max_ancestors-1, max_descendants-1, true /* include_partial_groups */), + if (AttemptSelection(value_to_select, CoinEligibilityFilter(0, 1, max_ancestors-1, max_descendants-1, true /* include_partial_groups */), vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) { return true; } // Try with unsafe inputs if they are allowed. This may spend unconfirmed outputs // received from other wallets. if (coin_control.m_include_unsafe_inputs - && SelectCoinsMinConf(value_to_select, + && AttemptSelection(value_to_select, CoinEligibilityFilter(0 /* conf_mine */, 0 /* conf_theirs */, max_ancestors-1, max_descendants-1, true /* include_partial_groups */), vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) { return true; @@ -512,7 +512,7 @@ bool CWallet::SelectCoins(const std::vector& vAvailableCoins, const CAm // Try with unlimited ancestors/descendants. The transaction will still need to meet // mempool ancestor/descendant policy to be accepted to mempool and broadcasted, but // OutputGroups use heuristics that may overestimate ancestor/descendant counts. - if (!fRejectLongChains && SelectCoinsMinConf(value_to_select, + if (!fRejectLongChains && AttemptSelection(value_to_select, CoinEligibilityFilter(0, 1, std::numeric_limits::max(), std::numeric_limits::max(), true /* include_partial_groups */), vCoins, setCoinsRet, nValueRet, coin_selection_params, nCoinType)) { return true; @@ -522,7 +522,7 @@ bool CWallet::SelectCoins(const std::vector& vAvailableCoins, const CAm return false; }(); - // SelectCoinsMinConf clears setCoinsRet, so add the preset inputs from coin_control to the coinset + // AttemptSelection clears setCoinsRet, so add the preset inputs from coin_control to the coinset util::insert(setCoinsRet, setPresetCoins); // add preset inputs to the total value selected @@ -602,309 +602,288 @@ bool CWallet::CreateTransactionInternal( bool sign, int nExtraPayloadSize) { - CAmount nValue = 0; + AssertLockHeld(cs_wallet); + + CMutableTransaction txNew; // The resulting transaction that we make + txNew.nLockTime = GetLocktimeForNewTransaction(chain(), GetLastBlockHash(), GetLastBlockHeight()); + + CoinSelectionParams coin_selection_params; // Parameters for coin selection, init with dummy + coin_selection_params.m_avoid_partial_spends = coin_control.m_avoid_partial_spends; + + CAmount recipients_sum = 0; ReserveDestination reservedest(this); const bool sort_bip69{nChangePosInOut == -1}; - unsigned int nSubtractFeeFromAmount = 0; - for (const auto& recipient : vecSend) - { - if (nValue < 0 || recipient.nAmount < 0) - { - error = _("Transaction amounts must not be negative"); - return false; + unsigned int outputs_to_subtract_fee_from = 0; // The number of outputs which we are subtracting the fee from + for (const auto& recipient : vecSend) { + recipients_sum += recipient.nAmount; + + if (recipient.fSubtractFeeFromAmount) { + outputs_to_subtract_fee_from++; + coin_selection_params.m_subtract_fee_outputs = true; } - nValue += recipient.nAmount; + } - if (recipient.fSubtractFeeFromAmount) - nSubtractFeeFromAmount++; + // Create change script that will be used if we need change + // TODO: pass in scriptChange instead of reservedest so + // change transaction isn't always pay-to-bitcoin-address + CScript scriptChange; + + // coin control: send change to custom address + if (!std::get_if(&coin_control.destChange)) { + scriptChange = GetScriptForDestination(coin_control.destChange); + } else { // no coin control: send change to newly generated address + // Note: We use a new key here to keep it from being obvious which side is the change. + // The drawback is that by not reusing a previous key, the change may be lost if a + // backup is restored, if the backup doesn't have the new private key for the change. + // If we reused the old key, it would be possible to add code to look for and + // rediscover unknown transactions that were written with keys of ours to recover + // post-backup change. + + // Reserve a new key pair from key pool. If it fails, provide a dummy + // destination in case we don't need change. + CTxDestination dest; + if (!reservedest.GetReservedDestination(dest, true)) { + error = _("Transaction needs a change address, but we can't generate it. Please call keypoolrefill first."); + } + scriptChange = GetScriptForDestination(dest); + // A valid destination implies a change script (and + // vice-versa). An empty change script will abort later, if the + // change keypool ran out, but change is required. + CHECK_NONFATAL(IsValidDestination(dest) != scriptChange.empty()); } - if (vecSend.empty()) - { - error = _("Transaction must have at least one recipient"); - return false; + CTxOut change_prototype_txout(0, scriptChange); + coin_selection_params.change_output_size = GetSerializeSize(change_prototype_txout); + + // Get size of spending the change output + int change_spend_size = CalculateMaximumSignedInputSize(change_prototype_txout, this); + // If the wallet doesn't know how to sign change output, assume p2sh-p2pkh + // as lower-bound to allow BnB to do it's thing + if (change_spend_size == -1) { + coin_selection_params.change_spend_size = DUMMY_NESTED_P2PKH_INPUT_SIZE; + } else { + coin_selection_params.change_spend_size = (size_t)change_spend_size; } - CMutableTransaction txNew; + // Set discard feerate + coin_selection_params.m_discard_feerate = coin_control.m_discard_feerate ? *coin_control.m_discard_feerate : GetDiscardRate(*this); + + // Get the fee rate to use effective values in coin selection FeeCalculation feeCalc; - int nBytes{0}; - CAmount fee_needed{0}; - { - std::set setCoins; - LOCK(cs_wallet); - txNew.nLockTime = GetLocktimeForNewTransaction(chain(), GetLastBlockHash(), GetLastBlockHeight()); - { - std::vector vAvailableCoins; - AvailableCoins(vAvailableCoins, &coin_control, 1, MAX_MONEY, MAX_MONEY, 0); - CoinSelectionParams coin_selection_params; // Parameters for coin selection, init with dummy - coin_selection_params.m_avoid_partial_spends = coin_control.m_avoid_partial_spends; - - // Create change script that will be used if we need change - // TODO: pass in scriptChange instead of reservedest so - // change transaction isn't always pay-to-bitcoin-address - CScript scriptChange; - - // coin control: send change to custom address - if (!std::get_if(&coin_control.destChange)) { - scriptChange = GetScriptForDestination(coin_control.destChange); - } else { // no coin control: send change to newly generated address - // Note: We use a new key here to keep it from being obvious which side is the change. - // The drawback is that by not reusing a previous key, the change may be lost if a - // backup is restored, if the backup doesn't have the new private key for the change. - // If we reused the old key, it would be possible to add code to look for and - // rediscover unknown transactions that were written with keys of ours to recover - // post-backup change. - - // Reserve a new key pair from key pool. If it fails, provide a dummy - // destination in case we don't need change. - CTxDestination dest; - if (!reservedest.GetReservedDestination(dest, true)) { - error = _("Transaction needs a change address, but we can't generate it. Please call keypoolrefill first."); - } - scriptChange = GetScriptForDestination(dest); - // A valid destination implies a change script (and - // vice-versa). An empty change script will abort later, if the - // change keypool ran out, but change is required. - CHECK_NONFATAL(IsValidDestination(dest) != scriptChange.empty()); - } - CTxOut change_prototype_txout(0, scriptChange); - coin_selection_params.change_output_size = GetSerializeSize(change_prototype_txout); - - // Get size of spending the change output - int change_spend_size = CalculateMaximumSignedInputSize(change_prototype_txout, this); - // If the wallet doesn't know how to sign change output, assume p2sh-p2pkh - // as lower-bound to allow BnB to do it's thing - if (change_spend_size == -1) { - coin_selection_params.change_spend_size = DUMMY_NESTED_P2PKH_INPUT_SIZE; - } else { - coin_selection_params.change_spend_size = (size_t)change_spend_size; - } + coin_selection_params.m_effective_feerate = GetMinimumFeeRate(*this, coin_control, &feeCalc); + // Do not, ever, assume that it's fine to change the fee rate if the user has explicitly + // provided one + if (coin_control.m_feerate && coin_selection_params.m_effective_feerate > *coin_control.m_feerate) { + error = strprintf(_("Fee rate (%s) is lower than the minimum fee rate setting (%s)"), coin_control.m_feerate->ToString(FeeEstimateMode::DUFF_B), coin_selection_params.m_effective_feerate.ToString(FeeEstimateMode::DUFF_B)); + return false; + } + if (feeCalc.reason == FeeReason::FALLBACK && !m_allow_fallback_fee) { + // eventually allow a fallback fee + error = _("Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable -fallbackfee."); + return false; + } - // Set discard feerate - coin_selection_params.m_discard_feerate = coin_control.m_discard_feerate ? *coin_control.m_discard_feerate : GetDiscardRate(*this); + // Get long term estimate + CCoinControl cc_temp; + cc_temp.m_confirm_target = chain().estimateMaxBlocks(); + coin_selection_params.m_long_term_feerate = GetMinimumFeeRate(*this, cc_temp, nullptr); + + // Calculate the cost of change + // Cost of change is the cost of creating the change output + cost of spending the change output in the future. + // For creating the change output now, we use the effective feerate. + // For spending the change output in the future, we use the discard feerate for now. + // So cost of change = (change output size * effective feerate) + (size of spending change output * discard feerate) + coin_selection_params.m_change_fee = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.change_output_size); + coin_selection_params.m_cost_of_change = coin_selection_params.m_discard_feerate.GetFee(coin_selection_params.change_spend_size) + coin_selection_params.m_change_fee; + + // vouts to the payees + if (!coin_selection_params.m_subtract_fee_outputs) { + coin_selection_params.tx_noinputs_size = 9; // Static vsize overhead + outputs vsize. 4 nVersion, 4 nLocktime, 1 input count + coin_selection_params.tx_noinputs_size += GetSizeOfCompactSize(vecSend.size()); // bytes for output count + } + for (const auto& recipient : vecSend) + { + CTxOut txout(recipient.nAmount, recipient.scriptPubKey); - // Get the fee rate to use effective values in coin selection - coin_selection_params.m_effective_feerate = GetMinimumFeeRate(*this, coin_control, &feeCalc); - // Do not, ever, assume that it's fine to change the fee rate if the user has explicitly - // provided one - if (coin_control.m_feerate && coin_selection_params.m_effective_feerate > *coin_control.m_feerate) { - error = strprintf(_("Fee rate (%s) is lower than the minimum fee rate setting (%s)"), coin_control.m_feerate->ToString(FeeEstimateMode::DUFF_B), coin_selection_params.m_effective_feerate.ToString(FeeEstimateMode::DUFF_B)); - return false; - } - if (feeCalc.reason == FeeReason::FALLBACK && !m_allow_fallback_fee) { - // eventually allow a fallback fee - error = _("Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable -fallbackfee."); - return false; - } + // Include the fee cost for outputs. + if (!coin_selection_params.m_subtract_fee_outputs) { + coin_selection_params.tx_noinputs_size += ::GetSerializeSize(txout, PROTOCOL_VERSION); + } - // Get long term estimate - CCoinControl cc_temp; - cc_temp.m_confirm_target = chain().estimateMaxBlocks(); - coin_selection_params.m_long_term_feerate = GetMinimumFeeRate(*this, cc_temp, nullptr); - - // Calculate the cost of change - // Cost of change is the cost of creating the change output + cost of spending the change output in the future. - // For creating the change output now, we use the effective feerate. - // For spending the change output in the future, we use the discard feerate for now. - // So cost of change = (change output size * effective feerate) + (size of spending change output * discard feerate) - coin_selection_params.m_change_fee = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.change_output_size); - coin_selection_params.m_cost_of_change = coin_selection_params.m_discard_feerate.GetFee(coin_selection_params.change_spend_size) + coin_selection_params.m_change_fee; - - coin_selection_params.m_subtract_fee_outputs = nSubtractFeeFromAmount != 0; // If we are doing subtract fee from recipient, don't use effective values - - // vouts to the payees - if (!coin_selection_params.m_subtract_fee_outputs) { - coin_selection_params.tx_noinputs_size = 9; // Static vsize overhead + outputs vsize. 4 nVersion, 4 nLocktime, 1 input count - coin_selection_params.tx_noinputs_size += GetSizeOfCompactSize(vecSend.size()); // bytes for output count - } - for (const auto& recipient : vecSend) - { - CTxOut txout(recipient.nAmount, recipient.scriptPubKey); + if (IsDust(txout, chain().relayDustFee())) + { + error = _("Transaction amount too small"); + return false; + } + txNew.vout.push_back(txout); + } - // Include the fee cost for outputs. - if (!coin_selection_params.m_subtract_fee_outputs) { - coin_selection_params.tx_noinputs_size += ::GetSerializeSize(txout, PROTOCOL_VERSION); - } + // Include the fees for things that aren't inputs, excluding the change output + const CAmount not_input_fees = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.tx_noinputs_size); + CAmount selection_target = recipients_sum + not_input_fees; + + // Get available coins + std::vector vAvailableCoins; + AvailableCoins(vAvailableCoins, &coin_control, 1, MAX_MONEY, MAX_MONEY, 0); + + // Choose coins to use + CAmount inputs_sum = 0; + std::set setCoins; + if (!SelectCoins(vAvailableCoins, /* nTargetValue */ selection_target, setCoins, inputs_sum, coin_control, coin_selection_params)) { + if (coin_control.nCoinType == CoinType::ONLY_NONDENOMINATED) { + error = _("Unable to locate enough non-denominated funds for this transaction."); + } else if (coin_control.nCoinType == CoinType::ONLY_FULLY_MIXED) { + error = _("Unable to locate enough mixed funds for this transaction."); + error = error + Untranslated(" ") + strprintf(_("%s uses exact denominated amounts to send funds, you might simply need to mix some more coins."), gCoinJoinName); + } else { + error = _("Insufficient funds."); + } + return false; + } - if (IsDust(txout, chain().relayDustFee())) - { - error = _("Transaction amount too small"); - return false; - } - txNew.vout.push_back(txout); - } + // Always make a change output + // We will reduce the fee from this change output later, and remove the output if it is too small. + const CAmount change_and_fee = inputs_sum - recipients_sum; + assert(change_and_fee >= 0); + CTxOut newTxOut(change_and_fee, scriptChange); - // Include the fees for things that aren't inputs, excluding the change output - const CAmount not_input_fees = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.tx_noinputs_size); - CAmount nValueToSelect = nValue + not_input_fees; - - // Choose coins to use - CAmount inputs_sum = 0; - setCoins.clear(); - if (!SelectCoins(vAvailableCoins, /* nTargetValue */ nValueToSelect, setCoins, inputs_sum, coin_control, coin_selection_params)) { - if (coin_control.nCoinType == CoinType::ONLY_NONDENOMINATED) { - error = _("Unable to locate enough non-denominated funds for this transaction."); - } else if (coin_control.nCoinType == CoinType::ONLY_FULLY_MIXED) { - error = _("Unable to locate enough mixed funds for this transaction."); - error = error + Untranslated(" ") + strprintf(_("%s uses exact denominated amounts to send funds, you might simply need to mix some more coins."), gCoinJoinName); - } else { - error = _("Insufficient funds."); - } - return false; - } + if (nChangePosInOut == -1) + { + // Insert change txn at random position: + nChangePosInOut = GetRandInt(txNew.vout.size()+1); + } + else if ((unsigned int)nChangePosInOut > txNew.vout.size()) + { + error = _("Transaction change output index out of range"); + return false; + } - // Always make a change output - // We will reduce the fee from this change output later, and remove the output if it is too small. - const CAmount change_and_fee = inputs_sum - nValue; - assert(change_and_fee >= 0); - CTxOut newTxOut(change_and_fee, scriptChange); + assert(nChangePosInOut != -1); + auto change_position = txNew.vout.insert(txNew.vout.begin() + nChangePosInOut, newTxOut); + + // We're making a copy of vecSend because it's const, sortedVecSend should be used + // in place of vecSend in all subsequent usage. + std::vector sortedVecSend{vecSend}; + if (sort_bip69) { + std::sort(txNew.vout.begin(), txNew.vout.end(), CompareOutputBIP69()); + // The output reduction loop uses vecSend to map to txNew.vout, we need to + // shuffle them both to ensure this mapping remains consistent + std::sort(sortedVecSend.begin(), sortedVecSend.end(), + [](const CRecipient& a, const CRecipient& b) { + return a.nAmount < b.nAmount || (a.nAmount == b.nAmount && a.scriptPubKey < b.scriptPubKey); + }); + + // If there was a change output added before, we must update its position now + if (const auto it = std::find(txNew.vout.begin(), txNew.vout.end(), newTxOut); it != txNew.vout.end()) { + change_position = it; + nChangePosInOut = std::distance(txNew.vout.begin(), change_position); + } + }; - if (nChangePosInOut == -1) - { - // Insert change txn at random position: - nChangePosInOut = GetRandInt(txNew.vout.size()+1); - } - else if ((unsigned int)nChangePosInOut > txNew.vout.size()) - { - error = _("Transaction change output index out of range"); - return false; - } + // Note how the sequence number is set to non-maxint so that + // the nLockTime set above actually works. + const uint32_t nSequence = CTxIn::SEQUENCE_FINAL - 1; + for (const auto& coin : setCoins) { + txNew.vin.push_back(CTxIn(coin.outpoint, CScript(), nSequence)); + } - assert(nChangePosInOut != -1); - auto change_position = txNew.vout.insert(txNew.vout.begin() + nChangePosInOut, newTxOut); - - // We're making a copy of vecSend because it's const, sortedVecSend should be used - // in place of vecSend in all subsequent usage. - std::vector sortedVecSend{vecSend}; - if (sort_bip69) { - std::sort(txNew.vout.begin(), txNew.vout.end(), CompareOutputBIP69()); - // The output reduction loop uses vecSend to map to txNew.vout, we need to - // shuffle them both to ensure this mapping remains consistent - std::sort(sortedVecSend.begin(), sortedVecSend.end(), - [](const CRecipient& a, const CRecipient& b) { - return a.nAmount < b.nAmount || (a.nAmount == b.nAmount && a.scriptPubKey < b.scriptPubKey); - }); - - // If there was a change output added before, we must update its position now - if (const auto it = std::find(txNew.vout.begin(), txNew.vout.end(), newTxOut); it != txNew.vout.end()) { - change_position = it; - nChangePosInOut = std::distance(txNew.vout.begin(), change_position); - } - }; + // Fill in final vin and shuffle/sort it + if (sort_bip69) { std::sort(txNew.vin.begin(), txNew.vin.end(), CompareInputBIP69()); } + else { Shuffle(txNew.vin.begin(), txNew.vin.end(), FastRandomContext()); } - // Dummy fill vin for maximum size estimation - // - for (const auto& coin : setCoins) { - txNew.vin.push_back(CTxIn(coin.outpoint, CScript())); - } + // Calculate the transaction fee + int nBytes = CalculateMaximumSignedTxSize(CTransaction(txNew), this, coin_control.fAllowWatchOnly); + if (nBytes < 0) { + error = _("Signing transaction failed"); + return false; + } - // Calculate the transaction fee - nBytes = CalculateMaximumSignedTxSize(CTransaction(txNew), this, coin_control.fAllowWatchOnly); - if (nBytes < 0) { - error = _("Signing transaction failed"); - return false; - } + if (nExtraPayloadSize != 0) { + // account for extra payload in fee calculation + nBytes += GetSizeOfCompactSize(nExtraPayloadSize) + nExtraPayloadSize; + } - if (nExtraPayloadSize != 0) { - // account for extra payload in fee calculation - nBytes += GetSizeOfCompactSize(nExtraPayloadSize) + nExtraPayloadSize; - } + CAmount fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes); - fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes); + if (!coin_selection_params.m_subtract_fee_outputs) { + change_position->nValue -= fee_needed; + } - if (nSubtractFeeFromAmount == 0) { - change_position->nValue -= fee_needed; - } + // We want to drop the change to fees if: + // 1. The change output would be dust + // 2. The change is within the (almost) exact match window, i.e. it is less than or equal to the cost of the change output (cost_of_change) + // 3. We are working with fully mixed CoinJoin denominations + CAmount change_amount = change_position->nValue; + if (IsDust(*change_position, coin_selection_params.m_discard_feerate) || change_amount <= coin_selection_params.m_cost_of_change || coin_control.nCoinType == CoinType::ONLY_FULLY_MIXED) + { + nChangePosInOut = -1; + change_amount = 0; + txNew.vout.erase(change_position); - // We want to drop the change to fees if: - // 1. The change output would be dust - // 2. The change is within the (almost) exact match window, i.e. it is less than or equal to the cost of the change output (cost_of_change) - // 3. We are working with fully mixed CoinJoin denominations - CAmount change_amount = change_position->nValue; - if (IsDust(*change_position, coin_selection_params.m_discard_feerate) || change_amount <= coin_selection_params.m_cost_of_change || coin_control.nCoinType == CoinType::ONLY_FULLY_MIXED) - { - nChangePosInOut = -1; - change_amount = 0; - txNew.vout.erase(change_position); + nBytes = CalculateMaximumSignedTxSize(CTransaction(txNew), this, coin_control.fAllowWatchOnly); + fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes); + } - nBytes = CalculateMaximumSignedTxSize(CTransaction(txNew), this, coin_control.fAllowWatchOnly); - fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes); - } + nFeeRet = inputs_sum - recipients_sum - change_amount; - nFeeRet = inputs_sum - nValue - change_amount; + // Update nFeeRet in case fee_needed changed due to dropping the change output + if (fee_needed <= change_and_fee - change_amount) { + nFeeRet = change_and_fee - change_amount; + } - // Update nFeeRet in case fee_needed changed due to dropping the change output - if (fee_needed <= change_and_fee - change_amount) { - nFeeRet = change_and_fee - change_amount; + // Reduce output values for subtractFeeFromAmount + if (coin_selection_params.m_subtract_fee_outputs) { + CAmount to_reduce = fee_needed + change_amount - change_and_fee; + int i = 0; + bool fFirst = true; + for (const auto& recipient : sortedVecSend) + { + if (i == nChangePosInOut) { + ++i; } + CTxOut& txout = txNew.vout[i]; + + if (recipient.fSubtractFeeFromAmount) + { + txout.nValue -= to_reduce / outputs_to_subtract_fee_from; // Subtract fee equally from each selected recipient - // Reduce output values for subtractFeeFromAmount - if (nSubtractFeeFromAmount != 0) { - CAmount to_reduce = fee_needed + change_amount - change_and_fee; - int i = 0; - bool fFirst = true; - for (const auto& recipient : sortedVecSend) + if (fFirst) // first receiver pays the remainder not divisible by output count { - if (i == nChangePosInOut) { - ++i; - } - CTxOut& txout = txNew.vout[i]; - - if (recipient.fSubtractFeeFromAmount) - { - txout.nValue -= to_reduce / nSubtractFeeFromAmount; // Subtract fee equally from each selected recipient - - if (fFirst) // first receiver pays the remainder not divisible by output count - { - fFirst = false; - txout.nValue -= to_reduce % nSubtractFeeFromAmount; - } - // Error if this output is reduced to be below dust - if (IsDust(txout, chain().relayDustFee())) { - if (txout.nValue < 0) { - error = _("The transaction amount is too small to pay the fee"); - } else { - error = _("The transaction amount is too small to send after the fee has been deducted"); - } - return false; - } + fFirst = false; + txout.nValue -= to_reduce % outputs_to_subtract_fee_from; + } + // Error if this output is reduced to be below dust + if (IsDust(txout, chain().relayDustFee())) { + if (txout.nValue < 0) { + error = _("The transaction amount is too small to pay the fee"); + } else { + error = _("The transaction amount is too small to send after the fee has been deducted"); } - ++i; + return false; } - nFeeRet = fee_needed; - } - - // Give up if change keypool ran out and change is required - if (scriptChange.empty() && nChangePosInOut != -1) { - return false; } + ++i; } + nFeeRet = fee_needed; + } - // Fill in final vin and shuffle/sort it - txNew.vin.clear(); - - // Note how the sequence number is set to non-maxint so that - // the nLockTime set above actually works. - const uint32_t nSequence = CTxIn::SEQUENCE_FINAL - 1; - for (const auto& coin : setCoins) { - txNew.vin.push_back(CTxIn(coin.outpoint, CScript(), nSequence)); - } - if (sort_bip69) { std::sort(txNew.vin.begin(), txNew.vin.end(), CompareInputBIP69()); } - else { Shuffle(txNew.vin.begin(), txNew.vin.end(), FastRandomContext()); } + // Give up if change keypool ran out and change is required + if (scriptChange.empty() && nChangePosInOut != -1) { + return false; + } - if (sign && !SignTransaction(txNew)) { - error = _("Signing transaction failed"); - return false; - } + if (sign && !SignTransaction(txNew)) { + error = _("Signing transaction failed"); + return false; + } - // Return the constructed transaction data. - tx = MakeTransactionRef(std::move(txNew)); + // Return the constructed transaction data. + tx = MakeTransactionRef(std::move(txNew)); - // Limit size - if ((sign && ::GetSerializeSize(*tx, PROTOCOL_VERSION) > MAX_STANDARD_TX_SIZE) || - (!sign && static_cast(nBytes) > MAX_STANDARD_TX_SIZE)) { - error = _("Transaction too large"); - return false; - } + // Limit size + if ((sign && ::GetSerializeSize(*tx, PROTOCOL_VERSION) > MAX_STANDARD_TX_SIZE) || + (!sign && static_cast(nBytes) > MAX_STANDARD_TX_SIZE)) { + error = _("Transaction too large"); + return false; } if (fee_needed > nFeeRet) { @@ -952,6 +931,18 @@ bool CWallet::CreateTransaction( bool sign, int nExtraPayloadSize) { + if (vecSend.empty()) { + error = _("Transaction must have at least one recipient"); + return false; + } + + if (std::any_of(vecSend.cbegin(), vecSend.cend(), [](const auto& recipient){ return recipient.nAmount < 0; })) { + error = _("Transaction amounts must not be negative"); + return false; + } + + LOCK(cs_wallet); + int nChangePosIn = nChangePosInOut; Assert(!tx); // tx is an out-param. TODO change the return type from bool to tx (or nullptr) bool res = CreateTransactionInternal(vecSend, tx, nFeeRet, nChangePosInOut, error, coin_control, fee_calc_out, sign, nExtraPayloadSize); diff --git a/src/wallet/test/coinselector_tests.cpp b/src/wallet/test/coinselector_tests.cpp index 1cf9cbde0b8b2..f59f587092a4b 100644 --- a/src/wallet/test/coinselector_tests.cpp +++ b/src/wallet/test/coinselector_tests.cpp @@ -254,7 +254,7 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, 2 * CENT, selection, value_ret)); } - // Make sure that effective value is working in SelectCoinsMinConf when BnB is used + // Make sure that effective value is working in AttemptSelection when BnB is used CoinSelectionParams coin_selection_params_bnb(/* change_output_size= */ 0, /* change_spend_size= */ 0, /* effective_feerate= */ CFeeRate(3000), /* long_term_feerate= */ CFeeRate(1000), /* discard_feerate= */ CFeeRate(1000), @@ -271,14 +271,14 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) add_coin(coins, *wallet, 1); coins.at(0).nInputBytes = 40; // Make sure that it has a negative effective value. The next check should assert if this somehow got through. Otherwise it will fail - BOOST_CHECK(!wallet->SelectCoinsMinConf(1 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params_bnb)); + BOOST_CHECK(!wallet->AttemptSelection(1 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params_bnb)); // Test fees subtracted from output: coins.clear(); add_coin(coins, *wallet, 1 * CENT); coins.at(0).nInputBytes = 40; coin_selection_params_bnb.m_subtract_fee_outputs = true; - BOOST_CHECK(wallet->SelectCoinsMinConf(1 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params_bnb)); + BOOST_CHECK(wallet->AttemptSelection(1 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params_bnb)); BOOST_CHECK_EQUAL(nValueRet, 1 * CENT); } @@ -320,24 +320,24 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) coins.clear(); // with an empty wallet we can't even pay one cent - BOOST_CHECK(!wallet->SelectCoinsMinConf( 1 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(!wallet->AttemptSelection( 1 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params)); add_coin(coins, *wallet, 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, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(!wallet->AttemptSelection( 1 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params)); // but we can find a new 1 cent - BOOST_CHECK(wallet->SelectCoinsMinConf( 1 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection( 1 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 1 * CENT); add_coin(coins, *wallet, 2*CENT); // add a mature 2 cent coin // we can't make 3 cents of mature coins - BOOST_CHECK(!wallet->SelectCoinsMinConf( 3 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(!wallet->AttemptSelection( 3 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params)); // we can make 3 cents of new coins - BOOST_CHECK(wallet->SelectCoinsMinConf( 3 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection( 3 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 3 * CENT); add_coin(coins, *wallet, 5*CENT); // add a mature 5 cent coin, @@ -347,33 +347,33 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) // 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, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(!wallet->AttemptSelection(38 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params)); // 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, filter_standard_extra, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(!wallet->AttemptSelection(38 * CENT, filter_standard_extra, coins, setCoinsRet, nValueRet, coin_selection_params)); // but we can make 37 cents if we accept new coins from ourself - BOOST_CHECK(wallet->SelectCoinsMinConf(37 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection(37 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 37 * CENT); // and we can make 38 cents if we accept all new coins - BOOST_CHECK(wallet->SelectCoinsMinConf(38 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection(38 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); 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, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection(34 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 35 * CENT); // but 35 cents is closest BOOST_CHECK_EQUAL(setCoinsRet.size(), 3U); // 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. We should see just 2+5 - BOOST_CHECK(wallet->SelectCoinsMinConf( 7 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection( 7 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); 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(wallet->SelectCoinsMinConf( 8 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection( 8 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); 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(wallet->SelectCoinsMinConf( 9 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection( 9 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 10 * CENT); BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); @@ -387,30 +387,30 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) add_coin(coins, *wallet, 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, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); - BOOST_CHECK(!wallet->SelectCoinsMinConf(72 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection(71 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(!wallet->AttemptSelection(72 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); // 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, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection(16 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 20 * CENT); // we should get 20 in one coin BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); add_coin(coins, *wallet, 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, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection(16 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 18 * CENT); // we should get 18 in 3 coins BOOST_CHECK_EQUAL(setCoinsRet.size(), 3U); add_coin(coins, *wallet, 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, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection(16 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 18 * CENT); // we should get 18 in 1 coin BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); // because in the event of a tie, the biggest coin wins // now try making 11 cents. we should get 5+6 - BOOST_CHECK(wallet->SelectCoinsMinConf(11 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection(11 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 11 * CENT); BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U); @@ -419,11 +419,11 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) add_coin(coins, *wallet, 2*COIN); add_coin(coins, *wallet, 3*COIN); add_coin(coins, *wallet, 4*COIN); // now we have 5+6+7+8+18+20+30+100+200+300+400 = 1094 cents - BOOST_CHECK(wallet->SelectCoinsMinConf(95 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection(95 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 1 * COIN); // we should get 1 BTC in 1 coin BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); - BOOST_CHECK(wallet->SelectCoinsMinConf(195 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection(195 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 2 * COIN); // we should get 2 BTC in 1 coin BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); @@ -438,14 +438,14 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) // 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(wallet->SelectCoinsMinConf(MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection(MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, MIN_CHANGE); // but if we add a bigger coin, small change is avoided add_coin(coins, *wallet, 1111*MIN_CHANGE); // try making 1 from 0.1 + 0.2 + 0.3 + 0.4 + 0.5 + 1111 = 1112.5 - BOOST_CHECK(wallet->SelectCoinsMinConf(1 * MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection(1 * MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 1 * MIN_CHANGE); // we should get the exact amount // if we add more small coins: @@ -453,7 +453,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) add_coin(coins, *wallet, MIN_CHANGE * 7 / 10); // and try again to make 1.0 * MIN_CHANGE - BOOST_CHECK(wallet->SelectCoinsMinConf(1 * MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection(1 * MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 1 * MIN_CHANGE); // we should get the exact amount // run the 'mtgox' test (see https://blockexplorer.com/tx/29a3efd3ef04f9153d47a990bd7b048a4b2d213daaa5fb8ed670fb85f13bdbcf) @@ -462,7 +462,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) for (int j = 0; j < 20; j++) add_coin(coins, *wallet, 50000 * COIN); - BOOST_CHECK(wallet->SelectCoinsMinConf(500000 * COIN, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection(500000 * COIN, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 500000 * COIN); // we should get the exact amount BOOST_CHECK_EQUAL(setCoinsRet.size(), 10U); // in ten coins @@ -475,7 +475,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) add_coin(coins, *wallet, MIN_CHANGE * 6 / 10); add_coin(coins, *wallet, MIN_CHANGE * 7 / 10); add_coin(coins, *wallet, 1111 * MIN_CHANGE); - BOOST_CHECK(wallet->SelectCoinsMinConf(1 * MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection(1 * MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 1111 * MIN_CHANGE); // we get the bigger coin BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); @@ -485,7 +485,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) add_coin(coins, *wallet, MIN_CHANGE * 6 / 10); add_coin(coins, *wallet, MIN_CHANGE * 8 / 10); add_coin(coins, *wallet, 1111 * MIN_CHANGE); - BOOST_CHECK(wallet->SelectCoinsMinConf(MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection(MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, MIN_CHANGE); // we should get the exact amount BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U); // in two coins 0.4+0.6 @@ -496,12 +496,12 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) add_coin(coins, *wallet, MIN_CHANGE * 100); // trying to make 100.01 from these three coins - BOOST_CHECK(wallet->SelectCoinsMinConf(MIN_CHANGE * 10001 / 100, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection(MIN_CHANGE * 10001 / 100, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, MIN_CHANGE * 10105 / 100); // we should get all coins 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(wallet->SelectCoinsMinConf(MIN_CHANGE * 9990 / 100, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection(MIN_CHANGE * 9990 / 100, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 101 * MIN_CHANGE); BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U); } @@ -515,7 +515,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) // We only create the wallet once to save time, but we still run the coin selection RUN_TESTS times. for (int i = 0; i < RUN_TESTS; i++) { - BOOST_CHECK(wallet->SelectCoinsMinConf(2000, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection(2000, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); if (amt - 2000 < MIN_CHANGE) { // needs more than one input: @@ -599,7 +599,7 @@ BOOST_AUTO_TEST_CASE(ApproximateBestSubset) add_coin(coins, *wallet, 1000 * COIN); add_coin(coins, *wallet, 3 * COIN); - BOOST_CHECK(wallet->SelectCoinsMinConf(1003 * COIN, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(wallet->AttemptSelection(1003 * COIN, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params)); BOOST_CHECK_EQUAL(nValueRet, 1003 * COIN); BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U); } diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index bb5534ea5d2af..5a1ca8b49b357 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -395,7 +395,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati // ScriptPubKeyMan::GetID. In many cases it will be the hash of an internal structure std::map> m_spk_managers; - bool CreateTransactionInternal(const std::vector& vecSend, CTransactionRef& tx, CAmount& nFeeRet, int& nChangePosInOut, bilingual_str& error, const CCoinControl& coin_control, FeeCalculation& fee_calc_out, bool sign, int nExtraPayloadSize); + bool CreateTransactionInternal(const std::vector& vecSend, CTransactionRef& tx, CAmount& nFeeRet, int& nChangePosInOut, bilingual_str& error, const CCoinControl& coin_control, FeeCalculation& fee_calc_out, bool sign, int nExtraPayloadSize) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); /** * Catch wallet up to current chain, scanning new blocks, updating the best @@ -540,7 +540,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati * param@[out] setCoinsRet Populated with the coins selected if successful. * param@[out] nValueRet Used to return the total value of selected coins. */ - bool SelectCoinsMinConf(const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, std::vector coins, std::set& setCoinsRet, CAmount& nValueRet, const CoinSelectionParams& coin_selection_params, CoinType nCoinType = CoinType::ALL_COINS) const; + bool AttemptSelection(const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, std::vector coins, std::set& setCoinsRet, CAmount& nValueRet, const CoinSelectionParams& coin_selection_params, CoinType nCoinType = CoinType::ALL_COINS) const; // Coin selection bool SelectTxDSInsByDenomination(int nDenom, CAmount nValueMax, std::vector& vecTxDSInRet); From fc16ce781689f469f9942da2be6c47f8113698a2 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Tue, 7 Jan 2025 07:59:48 +0000 Subject: [PATCH 04/16] merge bitcoin#22686: Use GetSelectionAmount in ApproximateBestSubset We aren't adding the `assert` check as it's superceded by a "Fee needed > fee paid" error that was already backported from c1a84f10 (bitcoin#26643). --- src/wallet/coinselection.cpp | 4 +- test/functional/rpc_fundrawtransaction.py | 55 +++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/wallet/coinselection.cpp b/src/wallet/coinselection.cpp index a657b282c3b07..0a908e934debf 100644 --- a/src/wallet/coinselection.cpp +++ b/src/wallet/coinselection.cpp @@ -199,7 +199,7 @@ static void ApproximateBestSubset(const std::vector& groups, const //the selection random. if (nPass == 0 ? insecure_rand.randbool() : !vfIncluded[i]) { - nTotal += groups[i].m_value; + nTotal += groups[i].GetSelectionAmount(); ++nTotalInputCount; vfIncluded[i] = true; if (nTotal >= nTargetValue) @@ -211,7 +211,7 @@ static void ApproximateBestSubset(const std::vector& groups, const nBestInputCount = nTotalInputCount; vfBest = vfIncluded; } - nTotal -= groups[i].m_value; + nTotal -= groups[i].GetSelectionAmount(); --nTotalInputCount; vfIncluded[i] = false; } diff --git a/test/functional/rpc_fundrawtransaction.py b/test/functional/rpc_fundrawtransaction.py index eeac900658be2..987b2633f88a2 100755 --- a/test/functional/rpc_fundrawtransaction.py +++ b/test/functional/rpc_fundrawtransaction.py @@ -99,6 +99,7 @@ def run_test(self): self.test_option_subtract_fee_from_outputs() self.test_subtract_fee_with_presets() self.test_include_unsafe() + self.test_22670() def test_change_position(self): """Ensure setting changePosition in fundraw with an exact match is handled properly.""" @@ -932,6 +933,60 @@ def test_include_unsafe(self): signedtx = wallet.signrawtransactionwithwallet(fundedtx['hex']) wallet.sendrawtransaction(signedtx['hex']) + def test_22670(self): + # In issue #22670, it was observed that ApproximateBestSubset may + # choose enough value to cover the target amount but not enough to cover the transaction fees. + # This leads to a transaction whose actual transaction feerate is lower than expected. + # However at normal feerates, the difference between the effective value and the real value + # that this bug is not detected because the transaction fee must be at least 0.01 BTC (the minimum change value). + # Otherwise the targeted minimum change value will be enough to cover the transaction fees that were not + # being accounted for. So the minimum relay fee is set to 0.1 BTC/kvB in this test. + self.log.info("Test issue 22670 ApproximateBestSubset bug") + # Make sure the default wallet will not be loaded when restarted with a high minrelaytxfee + self.nodes[0].unloadwallet(self.default_wallet_name, False) + feerate = Decimal("0.1") + self.restart_node(0, [f"-minrelaytxfee={feerate}", "-discardfee=0"]) # Set high minrelayfee, set discardfee to 0 for easier calculation + + self.nodes[0].loadwallet(self.default_wallet_name, True) + funds = self.nodes[0].get_wallet_rpc(self.default_wallet_name) + self.nodes[0].createwallet(wallet_name="tester") + tester = self.nodes[0].get_wallet_rpc("tester") + + # Because this test is specifically for ApproximateBestSubset, the target value must be greater + # than any single input available, and require more than 1 input. So we make 3 outputs + for i in range(0, 3): + funds.sendtoaddress(tester.getnewaddress(), 1) + self.generate(self.nodes[0], 1, sync_fun=self.no_op) + + # Create transactions in order to calculate fees for the target bounds that can trigger this bug + change_tx = tester.fundrawtransaction(tester.createrawtransaction([], [{funds.getnewaddress(): 1.5}])) + tx = tester.createrawtransaction([], [{funds.getnewaddress(): 2}]) + no_change_tx = tester.fundrawtransaction(tx, {"subtractFeeFromOutputs": [0]}) + + overhead_fees = feerate * len(tx) / 2 / 1000 + cost_of_change = change_tx["fee"] - no_change_tx["fee"] + fees = no_change_tx["fee"] + assert_greater_than(fees, 0.01) + + def do_fund_send(target): + create_tx = tester.createrawtransaction([], [{funds.getnewaddress(): lower_bound}]) + funded_tx = tester.fundrawtransaction(create_tx) + signed_tx = tester.signrawtransactionwithwallet(funded_tx["hex"]) + assert signed_tx["complete"] + decoded_tx = tester.decoderawtransaction(signed_tx["hex"]) + assert_equal(len(decoded_tx["vin"]), 3) + assert tester.testmempoolaccept([signed_tx["hex"]])[0]["allowed"] + + # We want to choose more value than is available in 2 inputs when considering the fee, + # but not enough to need 3 inputs when not considering the fee. + # So the target value must be at least 2.00000001 - fee. + lower_bound = Decimal("2.00000001") - fees + # The target value must be at most 2 - cost_of_change - not_input_fees - min_change (these are all + # included in the target before ApproximateBestSubset). + upper_bound = Decimal("2.0") - cost_of_change - overhead_fees - Decimal("0.01") + assert_greater_than_or_equal(upper_bound, lower_bound) + do_fund_send(lower_bound) + do_fund_send(upper_bound) if __name__ == '__main__': RawTransactionsTest().main() From 12cdd7745ba24b23644db23b2ce80432c88f6497 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Thu, 9 Jan 2025 04:54:02 +0000 Subject: [PATCH 05/16] merge bitcoin#22742: Use proper target in do_fund_send --- test/functional/rpc_fundrawtransaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/rpc_fundrawtransaction.py b/test/functional/rpc_fundrawtransaction.py index 987b2633f88a2..34ff51c7b3de5 100755 --- a/test/functional/rpc_fundrawtransaction.py +++ b/test/functional/rpc_fundrawtransaction.py @@ -969,7 +969,7 @@ def test_22670(self): assert_greater_than(fees, 0.01) def do_fund_send(target): - create_tx = tester.createrawtransaction([], [{funds.getnewaddress(): lower_bound}]) + create_tx = tester.createrawtransaction([], [{funds.getnewaddress(): target}]) funded_tx = tester.fundrawtransaction(create_tx) signed_tx = tester.signrawtransactionwithwallet(funded_tx["hex"]) assert signed_tx["complete"] From b68528270e90091b2ad908a740781af65e33b100 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Tue, 7 Jan 2025 12:40:36 +0000 Subject: [PATCH 06/16] refactor: move tests to match upstream order, Dash tests at the tail This is done to avoid conflicts in an upcoming backport which assume the tests are ordered as they are upstream. Validate with `git log -p -n1` ` --color-moved=dimmed_zebra`. --- src/wallet/test/wallet_tests.cpp | 867 ++++++++++++++++--------------- 1 file changed, 434 insertions(+), 433 deletions(-) diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index 80bd82defa1de..1e0b7e5d7f595 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -49,10 +49,6 @@ static_assert(WALLET_INCREMENTAL_RELAY_FEE >= DEFAULT_INCREMENTAL_RELAY_FEE, "wa BOOST_FIXTURE_TEST_SUITE(wallet_tests, WalletTestingSetup) -namespace { -constexpr CAmount fallbackFee = 1000; -} // anonymous namespace - static std::shared_ptr TestLoadWallet(interfaces::Chain* chain, interfaces::CoinJoin::Loader* coinjoin_loader) { DatabaseOptions options; @@ -327,83 +323,6 @@ BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup) } } -// Verify getaddressinfo RPC produces more or less expected results -BOOST_FIXTURE_TEST_CASE(rpc_getaddressinfo, TestChain100Setup) -{ - std::shared_ptr wallet = std::make_shared(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateMockWalletDatabase()); - wallet->SetupLegacyScriptPubKeyMan(); - AddWallet(wallet); - CoreContext context{m_node}; - JSONRPCRequest request; - request.context = context; - UniValue response; - - // test p2pkh - std::string addr; - BOOST_CHECK_NO_THROW(addr = ::getrawchangeaddress().HandleRequest(request).get_str()); - - request.params.clear(); - request.params.setArray(); - request.params.push_back(addr); - BOOST_CHECK_NO_THROW(response = ::getaddressinfo().HandleRequest(request).get_obj()); - - BOOST_CHECK_EQUAL(find_value(response, "ismine").get_bool(), true); - BOOST_CHECK_EQUAL(find_value(response, "solvable").get_bool(), true); - BOOST_CHECK_EQUAL(find_value(response, "iswatchonly").get_bool(), false); - BOOST_CHECK_EQUAL(find_value(response, "isscript").get_bool(), false); - BOOST_CHECK_EQUAL(find_value(response, "ischange").get_bool(), true); - BOOST_CHECK(find_value(response, "pubkeys").isNull()); - BOOST_CHECK(find_value(response, "addresses").isNull()); - BOOST_CHECK(find_value(response, "sigsrequired").isNull()); - BOOST_CHECK(find_value(response, "label").isNull()); - - // test p2sh/multisig - std::string addr1; - std::string addr2; - BOOST_CHECK_NO_THROW(addr1 = ::getnewaddress().HandleRequest(request).get_str()); - BOOST_CHECK_NO_THROW(addr2 = ::getnewaddress().HandleRequest(request).get_str()); - - UniValue keys; - keys.setArray(); - keys.push_back(addr1); - keys.push_back(addr2); - - request.params.clear(); - request.params.setArray(); - request.params.push_back(2); - request.params.push_back(keys); - - BOOST_CHECK_NO_THROW(response = ::addmultisigaddress().HandleRequest(request)); - - std::string multisig = find_value(response.get_obj(), "address").get_str(); - - request.params.clear(); - request.params.setArray(); - request.params.push_back(multisig); - BOOST_CHECK_NO_THROW(response = ::getaddressinfo().HandleRequest(request).get_obj()); - - BOOST_CHECK_EQUAL(find_value(response, "ismine").get_bool(), true); - BOOST_CHECK_EQUAL(find_value(response, "solvable").get_bool(), true); - BOOST_CHECK_EQUAL(find_value(response, "iswatchonly").get_bool(), false); - BOOST_CHECK_EQUAL(find_value(response, "isscript").get_bool(), true); - BOOST_CHECK_EQUAL(find_value(response, "ischange").get_bool(), false); - BOOST_CHECK_EQUAL(find_value(response, "sigsrequired").get_int(), 2); - BOOST_CHECK(find_value(response, "label").isNull()); - - UniValue labels = find_value(response, "labels").get_array(); - UniValue pubkeys = find_value(response, "pubkeys").get_array(); - UniValue addresses = find_value(response, "addresses").get_array(); - - BOOST_CHECK_EQUAL(labels.size(), 1); - BOOST_CHECK_EQUAL(labels[0].get_str(), ""); - BOOST_CHECK_EQUAL(addresses.size(), 2); - BOOST_CHECK_EQUAL(addresses[0].get_str(), addr1); - BOOST_CHECK_EQUAL(addresses[1].get_str(), addr2); - BOOST_CHECK_EQUAL(pubkeys.size(), 2); - - RemoveWallet(wallet, std::nullopt); -} - // Check that GetImmatureCredit() returns a newly calculated value instead of // the cached value after a MarkDirty() call. // @@ -684,137 +603,452 @@ BOOST_FIXTURE_TEST_CASE(ListCoins, ListCoinsTestingSetup) BOOST_CHECK_EQUAL(list.begin()->second.size(), 2U); } -class CreateTransactionTestSetup : public TestChain100Setup +BOOST_FIXTURE_TEST_CASE(wallet_disableprivkeys, TestChain100Setup) { -public: - enum ChangeTest { - Skip, - NoChangeExpected, - ChangeExpected, - }; + NodeContext node; + node.fee_estimator = std::make_unique(); + node.mempool = std::make_unique(node.fee_estimator.get()); + auto chain = interfaces::MakeChain(node); + std::shared_ptr wallet = std::make_shared(chain.get(), m_node.coinjoin_loader.get(), "", CreateDummyWalletDatabase()); + wallet->SetupLegacyScriptPubKeyMan(); + wallet->SetMinVersion(FEATURE_LATEST); + wallet->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS); + BOOST_CHECK(!wallet->TopUpKeyPool(1000)); + CTxDestination dest; + bilingual_str error; + BOOST_CHECK(!wallet->GetNewDestination("", dest, error)); +} - // Result strings to test - const std::string strInsufficientFunds = "Insufficient funds."; - const std::string strAmountNotNegative = "Transaction amounts must not be negative"; - const std::string strAtLeastOneRecipient = "Transaction must have at least one recipient"; - const std::string strTooSmallToPayFee = "The transaction amount is too small to pay the fee"; - const std::string strTooSmallAfterFee = "The transaction amount is too small to send after the fee has been deducted"; - const std::string strTooSmall = "Transaction amount too small"; - const std::string strUnableToLocateCoinJoin1 = "Unable to locate enough non-denominated funds for this transaction."; - const std::string strUnableToLocateCoinJoin2 = "Unable to locate enough mixed funds for this transaction. CoinJoin uses exact denominated amounts to send funds, you might simply need to mix some more coins."; - const std::string strTransactionTooLarge = "Transaction too large"; - const std::string strChangeIndexOutOfRange = "Transaction change output index out of range"; - const std::string strExceededMaxTries = "Exceeded max tries."; - const std::string strMaxFeeExceeded = "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)"; +// Explicit calculation which is used to test the wallet constant +// We get the same virtual size due to rounding(weight/4) for both use_max_sig values +static size_t CalculateNestedKeyhashInputSize(bool use_max_sig) +{ + // Generate ephemeral valid pubkey + CKey key; + key.MakeNewKey(true); + CPubKey pubkey = key.GetPubKey(); - CreateTransactionTestSetup() - { - CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); - wallet = std::make_unique(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateMockWalletDatabase()); - wallet->LoadWallet(); - AddWallet(wallet); - AddKey(*wallet, coinbaseKey); - WalletRescanReserver reserver(*wallet); - reserver.reserve(); - { - LOCK(wallet->cs_wallet); - wallet->SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); - } - CWallet::ScanResult result = wallet->ScanForWalletTransactions(m_node.chainman->ActiveChain().Genesis()->GetBlockHash(), 0 /* start_height */, {} /* max_height */, reserver, false /* update */); - BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS); - } + // Generate pubkey hash + uint160 key_hash(Hash160(pubkey)); - ~CreateTransactionTestSetup() - { - RemoveWallet(wallet, std::nullopt); - } + // Create inner-script to enter into keystore. Key hash can't be 0... + CScript inner_script = CScript() << OP_0 << std::vector(key_hash.begin(), key_hash.end()); - std::shared_ptr wallet; - CCoinControl coinControl; + // Create outer P2SH script for the output + CScript script_pubkey = GetScriptForRawPubKey(pubkey); - template - bool CheckEqual(const T expected, const T actual) - { - BOOST_CHECK_EQUAL(expected, actual); - return expected == actual; - } + NodeContext node; + node.fee_estimator = std::make_unique(); + node.mempool = std::make_unique(node.fee_estimator.get()); + auto chain = interfaces::MakeChain(node); + CWallet wallet(chain.get(), /*coinjoin_loader=*/ nullptr, "", CreateDummyWalletDatabase()); + AddKey(wallet, key); + auto spk_man = wallet.GetLegacyScriptPubKeyMan(); + spk_man->AddCScript(inner_script); - bool CreateTransaction(const std::vector>& vecEntries, bool fCreateShouldSucceed = true, ChangeTest changeTest = ChangeTest::Skip) - { - return CreateTransaction(vecEntries, {}, -1, fCreateShouldSucceed, changeTest); - } - bool CreateTransaction(const std::vector>& vecEntries, std::string strErrorExpected, bool fCreateShouldSucceed = true, ChangeTest changeTest = ChangeTest::Skip) - { - return CreateTransaction(vecEntries, strErrorExpected, -1, fCreateShouldSucceed, changeTest); + // Fill in dummy signatures for fee calculation. + SignatureData sig_data; + + if (!ProduceSignature(*spk_man, use_max_sig ? DUMMY_MAXIMUM_SIGNATURE_CREATOR : DUMMY_SIGNATURE_CREATOR, script_pubkey, sig_data)) { + // We're hand-feeding it correct arguments; shouldn't happen + assert(false); } - bool CreateTransaction(const std::vector>& vecEntries, std::string strErrorExpected, int nChangePosRequest = -1, bool fCreateShouldSucceed = true, ChangeTest changeTest = ChangeTest::Skip) - { - CTransactionRef tx; - CAmount nFeeRet; - int nChangePos = nChangePosRequest; - bilingual_str strError; + CTxIn tx_in; + UpdateInput(tx_in, sig_data); + return ::GetSerializeSize(tx_in, PROTOCOL_VERSION); +} - FeeCalculation fee_calc_out; - bool fCreationSucceeded = wallet->CreateTransaction(GetRecipients(vecEntries), tx, nFeeRet, nChangePos, strError, coinControl, fee_calc_out); - bool fHitMaxTries = strError.original == strExceededMaxTries; - // This should never happen. - if (fHitMaxTries) { - BOOST_CHECK(!fHitMaxTries); - return false; - } - // Verify the creation succeeds if expected and fails if not. - if (!CheckEqual(fCreateShouldSucceed, fCreationSucceeded)) { - return false; - } - // Verify the expected error string if there is one provided - if (strErrorExpected.size() && !CheckEqual(strErrorExpected, strError.original)) { - return false; - } - if (!fCreateShouldSucceed) { - // No need to evaluate the following if the creation should have failed. - return true; - } - // Verify there is no change output if there wasn't any expected - bool fChangeTestPassed = changeTest == ChangeTest::Skip || - (changeTest == ChangeTest::ChangeExpected && nChangePos != -1) || - (changeTest == ChangeTest::NoChangeExpected && nChangePos == -1); - BOOST_CHECK(fChangeTestPassed); - if (!fChangeTestPassed) { - return false; - } - // Verify the change is at the requested position if there was a request - if (nChangePosRequest != -1 && !CheckEqual(nChangePosRequest, nChangePos)) { - return false; - } - // Verify the number of requested outputs does match the resulting outputs - return CheckEqual(vecEntries.size(), tx->vout.size() - (nChangePos != -1 ? 1 : 0)); - } +BOOST_FIXTURE_TEST_CASE(dummy_input_size_test, TestChain100Setup) +{ + BOOST_CHECK_EQUAL(CalculateNestedKeyhashInputSize(false), DUMMY_NESTED_P2PKH_INPUT_SIZE); + BOOST_CHECK_EQUAL(CalculateNestedKeyhashInputSize(true), DUMMY_NESTED_P2PKH_INPUT_SIZE + 1); +} - std::vector GetRecipients(const std::vector>& vecEntries) - { - CoreContext context{m_node}; - std::vector vecRecipients; - for (auto entry : vecEntries) { - JSONRPCRequest request; - request.context = context; - vecRecipients.push_back({GetScriptForDestination(DecodeDestination(getnewaddress().HandleRequest(request).get_str())), entry.first, entry.second}); - } - return vecRecipients; - } +bool malformed_descriptor(std::ios_base::failure e) +{ + std::string s(e.what()); + return s.find("Missing checksum") != std::string::npos; +} - std::vector GetCoins(const std::vector>& vecEntries) - { - CTransactionRef tx; - CAmount nFeeRet; - int nChangePosRet = -1; - bilingual_str strError; - CCoinControl coinControl; - FeeCalculation fee_calc_out; - BOOST_CHECK(wallet->CreateTransaction(GetRecipients(vecEntries), tx, nFeeRet, nChangePosRet, strError, coinControl, fee_calc_out)); - wallet->CommitTransaction(tx, {}, {}); - CMutableTransaction blocktx; - { +BOOST_FIXTURE_TEST_CASE(wallet_descriptor_test, BasicTestingSetup) +{ + std::vector malformed_record; + CVectorWriter vw(0, 0, malformed_record, 0); + vw << std::string("notadescriptor"); + vw << (uint64_t)0; + vw << (int32_t)0; + vw << (int32_t)0; + vw << (int32_t)1; + + SpanReader vr{0, 0, malformed_record, 0}; + WalletDescriptor w_desc; + BOOST_CHECK_EXCEPTION(vr >> w_desc, std::ios_base::failure, malformed_descriptor); +} + +//! Test CWallet::Create() and its behavior handling potential race +//! conditions if it's called the same time an incoming transaction shows up in +//! the mempool or a new block. +//! +//! It isn't possible to verify there aren't race condition in every case, so +//! this test just checks two specific cases and ensures that timing of +//! notifications in these cases doesn't prevent the wallet from detecting +//! transactions. +//! +//! In the first case, block and mempool transactions are created before the +//! wallet is loaded, but notifications about these transactions are delayed +//! until after it is loaded. The notifications are superfluous in this case, so +//! the test verifies the transactions are detected before they arrive. +//! +//! In the second case, block and mempool transactions are created after the +//! wallet rescan and notifications are immediately synced, to verify the wallet +//! must already have a handler in place for them, and there's no gap after +//! rescanning where new transactions in new blocks could be lost. +BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup) +{ + gArgs.ForceSetArg("-unsafesqlitesync", "1"); + // Create new wallet with known key and unload it. + auto wallet = TestLoadWallet(m_node.chain.get(), m_node.coinjoin_loader.get()); + CKey key; + key.MakeNewKey(true); + AddKey(*wallet, key); + TestUnloadWallet(std::move(wallet)); + + + // Add log hook to detect AddToWallet events from rescans, blockConnected, + // and transactionAddedToMempool notifications + int addtx_count = 0; + DebugLogHelper addtx_counter("[default wallet] AddToWallet", [&](const std::string* s) { + if (s) ++addtx_count; + return false; + }); + + + bool rescan_completed = false; + DebugLogHelper rescan_check("[default wallet] Rescan completed", [&](const std::string* s) { + if (s) rescan_completed = true; + return false; + }); + + + // Block the queue to prevent the wallet receiving blockConnected and + // transactionAddedToMempool notifications, and create block and mempool + // transactions paying to the wallet + std::promise promise; + CallFunctionInValidationInterfaceQueue([&promise] { + promise.get_future().wait(); + }); + bilingual_str error; + m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); + auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); + m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); + auto mempool_tx = TestSimpleSpend(*m_coinbase_txns[1], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); + BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, false, error)); + + + // Reload wallet and make sure new transactions are detected despite events + // being blocked + wallet = TestLoadWallet(m_node.chain.get(), m_node.coinjoin_loader.get()); + BOOST_CHECK(rescan_completed); + BOOST_CHECK_EQUAL(addtx_count, 2); + { + LOCK(wallet->cs_wallet); + BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_tx.GetHash()), 1U); + BOOST_CHECK_EQUAL(wallet->mapWallet.count(mempool_tx.GetHash()), 1U); + } + + + // Unblock notification queue and make sure stale blockConnected and + // transactionAddedToMempool events are processed + promise.set_value(); + SyncWithValidationInterfaceQueue(); + BOOST_CHECK_EQUAL(addtx_count, 4); + + TestUnloadWallet(std::move(wallet)); + + + // Load wallet again, this time creating new block and mempool transactions + // paying to the wallet as the wallet finishes loading and syncing the + // queue so the events have to be handled immediately. Releasing the wallet + // lock during the sync is a little artificial but is needed to avoid a + // deadlock during the sync and simulates a new block notification happening + // as soon as possible. + addtx_count = 0; + auto handler = HandleLoadWallet([&](std::unique_ptr wallet) { + BOOST_CHECK(rescan_completed); + m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); + block_tx = TestSimpleSpend(*m_coinbase_txns[2], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); + m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); + mempool_tx = TestSimpleSpend(*m_coinbase_txns[3], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); + BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, false, error)); + SyncWithValidationInterfaceQueue(); + }); + wallet = TestLoadWallet(m_node.chain.get(), m_node.coinjoin_loader.get()); + BOOST_CHECK_EQUAL(addtx_count, 4); + { + LOCK(wallet->cs_wallet); + BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_tx.GetHash()), 1U); + BOOST_CHECK_EQUAL(wallet->mapWallet.count(mempool_tx.GetHash()), 1U); + } + + TestUnloadWallet(std::move(wallet)); +} + +BOOST_FIXTURE_TEST_CASE(CreateWalletWithoutChain, BasicTestingSetup) +{ + // TODO: FIX FIX FIX - coinjoin_loader is null heere! + auto wallet = TestLoadWallet(nullptr, nullptr); + BOOST_CHECK(wallet); + UnloadWallet(std::move(wallet)); +} + +BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup) +{ + gArgs.ForceSetArg("-unsafesqlitesync", "1"); + auto chain = interfaces::MakeChain(m_node); + auto wallet = TestLoadWallet(m_node.chain.get(), m_node.coinjoin_loader.get()); + CKey key; + key.MakeNewKey(true); + AddKey(*wallet, key); + + bilingual_str error; + m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); + auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); + CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); + + SyncWithValidationInterfaceQueue(); + + { + auto block_hash = block_tx.GetHash(); + auto prev_hash = m_coinbase_txns[0]->GetHash(); + + LOCK(wallet->cs_wallet); + BOOST_CHECK(wallet->HasWalletSpend(prev_hash)); + BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 1u); + + std::vector vHashIn{ block_hash }, vHashOut; + BOOST_CHECK_EQUAL(wallet->ZapSelectTx(vHashIn, vHashOut), DBErrors::LOAD_OK); + + BOOST_CHECK(!wallet->HasWalletSpend(prev_hash)); + BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 0u); + } + + TestUnloadWallet(std::move(wallet)); +} + +/* --------------------------- Dash-specific tests start here --------------------------- */ +namespace { +constexpr CAmount fallbackFee = 1000; +} // anonymous namespace + +// Verify getaddressinfo RPC produces more or less expected results +BOOST_FIXTURE_TEST_CASE(rpc_getaddressinfo, TestChain100Setup) +{ + std::shared_ptr wallet = std::make_shared(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateMockWalletDatabase()); + wallet->SetupLegacyScriptPubKeyMan(); + AddWallet(wallet); + CoreContext context{m_node}; + JSONRPCRequest request; + request.context = context; + UniValue response; + + // test p2pkh + std::string addr; + BOOST_CHECK_NO_THROW(addr = ::getrawchangeaddress().HandleRequest(request).get_str()); + + request.params.clear(); + request.params.setArray(); + request.params.push_back(addr); + BOOST_CHECK_NO_THROW(response = ::getaddressinfo().HandleRequest(request).get_obj()); + + BOOST_CHECK_EQUAL(find_value(response, "ismine").get_bool(), true); + BOOST_CHECK_EQUAL(find_value(response, "solvable").get_bool(), true); + BOOST_CHECK_EQUAL(find_value(response, "iswatchonly").get_bool(), false); + BOOST_CHECK_EQUAL(find_value(response, "isscript").get_bool(), false); + BOOST_CHECK_EQUAL(find_value(response, "ischange").get_bool(), true); + BOOST_CHECK(find_value(response, "pubkeys").isNull()); + BOOST_CHECK(find_value(response, "addresses").isNull()); + BOOST_CHECK(find_value(response, "sigsrequired").isNull()); + BOOST_CHECK(find_value(response, "label").isNull()); + + // test p2sh/multisig + std::string addr1; + std::string addr2; + BOOST_CHECK_NO_THROW(addr1 = ::getnewaddress().HandleRequest(request).get_str()); + BOOST_CHECK_NO_THROW(addr2 = ::getnewaddress().HandleRequest(request).get_str()); + + UniValue keys; + keys.setArray(); + keys.push_back(addr1); + keys.push_back(addr2); + + request.params.clear(); + request.params.setArray(); + request.params.push_back(2); + request.params.push_back(keys); + + BOOST_CHECK_NO_THROW(response = ::addmultisigaddress().HandleRequest(request)); + + std::string multisig = find_value(response.get_obj(), "address").get_str(); + + request.params.clear(); + request.params.setArray(); + request.params.push_back(multisig); + BOOST_CHECK_NO_THROW(response = ::getaddressinfo().HandleRequest(request).get_obj()); + + BOOST_CHECK_EQUAL(find_value(response, "ismine").get_bool(), true); + BOOST_CHECK_EQUAL(find_value(response, "solvable").get_bool(), true); + BOOST_CHECK_EQUAL(find_value(response, "iswatchonly").get_bool(), false); + BOOST_CHECK_EQUAL(find_value(response, "isscript").get_bool(), true); + BOOST_CHECK_EQUAL(find_value(response, "ischange").get_bool(), false); + BOOST_CHECK_EQUAL(find_value(response, "sigsrequired").get_int(), 2); + BOOST_CHECK(find_value(response, "label").isNull()); + + UniValue labels = find_value(response, "labels").get_array(); + UniValue pubkeys = find_value(response, "pubkeys").get_array(); + UniValue addresses = find_value(response, "addresses").get_array(); + + BOOST_CHECK_EQUAL(labels.size(), 1); + BOOST_CHECK_EQUAL(labels[0].get_str(), ""); + BOOST_CHECK_EQUAL(addresses.size(), 2); + BOOST_CHECK_EQUAL(addresses[0].get_str(), addr1); + BOOST_CHECK_EQUAL(addresses[1].get_str(), addr2); + BOOST_CHECK_EQUAL(pubkeys.size(), 2); + + RemoveWallet(wallet, std::nullopt); +} + +class CreateTransactionTestSetup : public TestChain100Setup +{ +public: + enum ChangeTest { + Skip, + NoChangeExpected, + ChangeExpected, + }; + + // Result strings to test + const std::string strInsufficientFunds = "Insufficient funds."; + const std::string strAmountNotNegative = "Transaction amounts must not be negative"; + const std::string strAtLeastOneRecipient = "Transaction must have at least one recipient"; + const std::string strTooSmallToPayFee = "The transaction amount is too small to pay the fee"; + const std::string strTooSmallAfterFee = "The transaction amount is too small to send after the fee has been deducted"; + const std::string strTooSmall = "Transaction amount too small"; + const std::string strUnableToLocateCoinJoin1 = "Unable to locate enough non-denominated funds for this transaction."; + const std::string strUnableToLocateCoinJoin2 = "Unable to locate enough mixed funds for this transaction. CoinJoin uses exact denominated amounts to send funds, you might simply need to mix some more coins."; + const std::string strTransactionTooLarge = "Transaction too large"; + const std::string strChangeIndexOutOfRange = "Transaction change output index out of range"; + const std::string strExceededMaxTries = "Exceeded max tries."; + const std::string strMaxFeeExceeded = "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)"; + + CreateTransactionTestSetup() + { + CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); + wallet = std::make_unique(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateMockWalletDatabase()); + wallet->LoadWallet(); + AddWallet(wallet); + AddKey(*wallet, coinbaseKey); + WalletRescanReserver reserver(*wallet); + reserver.reserve(); + { + LOCK(wallet->cs_wallet); + wallet->SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); + } + CWallet::ScanResult result = wallet->ScanForWalletTransactions(m_node.chainman->ActiveChain().Genesis()->GetBlockHash(), 0 /* start_height */, {} /* max_height */, reserver, false /* update */); + BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS); + } + + ~CreateTransactionTestSetup() + { + RemoveWallet(wallet, std::nullopt); + } + + std::shared_ptr wallet; + CCoinControl coinControl; + + template + bool CheckEqual(const T expected, const T actual) + { + BOOST_CHECK_EQUAL(expected, actual); + return expected == actual; + } + + bool CreateTransaction(const std::vector>& vecEntries, bool fCreateShouldSucceed = true, ChangeTest changeTest = ChangeTest::Skip) + { + return CreateTransaction(vecEntries, {}, -1, fCreateShouldSucceed, changeTest); + } + bool CreateTransaction(const std::vector>& vecEntries, std::string strErrorExpected, bool fCreateShouldSucceed = true, ChangeTest changeTest = ChangeTest::Skip) + { + return CreateTransaction(vecEntries, strErrorExpected, -1, fCreateShouldSucceed, changeTest); + } + + bool CreateTransaction(const std::vector>& vecEntries, std::string strErrorExpected, int nChangePosRequest = -1, bool fCreateShouldSucceed = true, ChangeTest changeTest = ChangeTest::Skip) + { + CTransactionRef tx; + CAmount nFeeRet; + int nChangePos = nChangePosRequest; + bilingual_str strError; + + FeeCalculation fee_calc_out; + bool fCreationSucceeded = wallet->CreateTransaction(GetRecipients(vecEntries), tx, nFeeRet, nChangePos, strError, coinControl, fee_calc_out); + bool fHitMaxTries = strError.original == strExceededMaxTries; + // This should never happen. + if (fHitMaxTries) { + BOOST_CHECK(!fHitMaxTries); + return false; + } + // Verify the creation succeeds if expected and fails if not. + if (!CheckEqual(fCreateShouldSucceed, fCreationSucceeded)) { + return false; + } + // Verify the expected error string if there is one provided + if (strErrorExpected.size() && !CheckEqual(strErrorExpected, strError.original)) { + return false; + } + if (!fCreateShouldSucceed) { + // No need to evaluate the following if the creation should have failed. + return true; + } + // Verify there is no change output if there wasn't any expected + bool fChangeTestPassed = changeTest == ChangeTest::Skip || + (changeTest == ChangeTest::ChangeExpected && nChangePos != -1) || + (changeTest == ChangeTest::NoChangeExpected && nChangePos == -1); + BOOST_CHECK(fChangeTestPassed); + if (!fChangeTestPassed) { + return false; + } + // Verify the change is at the requested position if there was a request + if (nChangePosRequest != -1 && !CheckEqual(nChangePosRequest, nChangePos)) { + return false; + } + // Verify the number of requested outputs does match the resulting outputs + return CheckEqual(vecEntries.size(), tx->vout.size() - (nChangePos != -1 ? 1 : 0)); + } + + std::vector GetRecipients(const std::vector>& vecEntries) + { + CoreContext context{m_node}; + std::vector vecRecipients; + for (auto entry : vecEntries) { + JSONRPCRequest request; + request.context = context; + vecRecipients.push_back({GetScriptForDestination(DecodeDestination(getnewaddress().HandleRequest(request).get_str())), entry.first, entry.second}); + } + return vecRecipients; + } + + std::vector GetCoins(const std::vector>& vecEntries) + { + CTransactionRef tx; + CAmount nFeeRet; + int nChangePosRet = -1; + bilingual_str strError; + CCoinControl coinControl; + FeeCalculation fee_calc_out; + BOOST_CHECK(wallet->CreateTransaction(GetRecipients(vecEntries), tx, nFeeRet, nChangePosRet, strError, coinControl, fee_calc_out)); + wallet->CommitTransaction(tx, {}, {}); + CMutableTransaction blocktx; + { LOCK(wallet->cs_wallet); blocktx = CMutableTransaction(*tx); } @@ -1189,237 +1423,4 @@ BOOST_FIXTURE_TEST_CASE(select_coins_grouped_by_addresses, ListCoinsTestingSetup BOOST_CHECK_EQUAL(wallet->GetAvailableBalance(), (500 + 499) * COIN); } -BOOST_FIXTURE_TEST_CASE(wallet_disableprivkeys, TestChain100Setup) -{ - NodeContext node; - node.fee_estimator = std::make_unique(); - node.mempool = std::make_unique(node.fee_estimator.get()); - auto chain = interfaces::MakeChain(node); - std::shared_ptr wallet = std::make_shared(chain.get(), m_node.coinjoin_loader.get(), "", CreateDummyWalletDatabase()); - wallet->SetupLegacyScriptPubKeyMan(); - wallet->SetMinVersion(FEATURE_LATEST); - wallet->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS); - BOOST_CHECK(!wallet->TopUpKeyPool(1000)); - CTxDestination dest; - bilingual_str error; - BOOST_CHECK(!wallet->GetNewDestination("", dest, error)); -} - -//! Test CWallet::Create() and its behavior handling potential race -//! conditions if it's called the same time an incoming transaction shows up in -//! the mempool or a new block. -//! -//! It isn't possible to verify there aren't race condition in every case, so -//! this test just checks two specific cases and ensures that timing of -//! notifications in these cases doesn't prevent the wallet from detecting -//! transactions. -//! -//! In the first case, block and mempool transactions are created before the -//! wallet is loaded, but notifications about these transactions are delayed -//! until after it is loaded. The notifications are superfluous in this case, so -//! the test verifies the transactions are detected before they arrive. -//! -//! In the second case, block and mempool transactions are created after the -//! wallet rescan and notifications are immediately synced, to verify the wallet -//! must already have a handler in place for them, and there's no gap after -//! rescanning where new transactions in new blocks could be lost. -BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup) -{ - gArgs.ForceSetArg("-unsafesqlitesync", "1"); - // Create new wallet with known key and unload it. - auto wallet = TestLoadWallet(m_node.chain.get(), m_node.coinjoin_loader.get()); - CKey key; - key.MakeNewKey(true); - AddKey(*wallet, key); - TestUnloadWallet(std::move(wallet)); - - - // Add log hook to detect AddToWallet events from rescans, blockConnected, - // and transactionAddedToMempool notifications - int addtx_count = 0; - DebugLogHelper addtx_counter("[default wallet] AddToWallet", [&](const std::string* s) { - if (s) ++addtx_count; - return false; - }); - - - bool rescan_completed = false; - DebugLogHelper rescan_check("[default wallet] Rescan completed", [&](const std::string* s) { - if (s) rescan_completed = true; - return false; - }); - - - // Block the queue to prevent the wallet receiving blockConnected and - // transactionAddedToMempool notifications, and create block and mempool - // transactions paying to the wallet - std::promise promise; - CallFunctionInValidationInterfaceQueue([&promise] { - promise.get_future().wait(); - }); - bilingual_str error; - m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); - auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); - m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); - auto mempool_tx = TestSimpleSpend(*m_coinbase_txns[1], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); - BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, false, error)); - - - // Reload wallet and make sure new transactions are detected despite events - // being blocked - wallet = TestLoadWallet(m_node.chain.get(), m_node.coinjoin_loader.get()); - BOOST_CHECK(rescan_completed); - BOOST_CHECK_EQUAL(addtx_count, 2); - { - LOCK(wallet->cs_wallet); - BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_tx.GetHash()), 1U); - BOOST_CHECK_EQUAL(wallet->mapWallet.count(mempool_tx.GetHash()), 1U); - } - - - // Unblock notification queue and make sure stale blockConnected and - // transactionAddedToMempool events are processed - promise.set_value(); - SyncWithValidationInterfaceQueue(); - BOOST_CHECK_EQUAL(addtx_count, 4); - - TestUnloadWallet(std::move(wallet)); - - - // Load wallet again, this time creating new block and mempool transactions - // paying to the wallet as the wallet finishes loading and syncing the - // queue so the events have to be handled immediately. Releasing the wallet - // lock during the sync is a little artificial but is needed to avoid a - // deadlock during the sync and simulates a new block notification happening - // as soon as possible. - addtx_count = 0; - auto handler = HandleLoadWallet([&](std::unique_ptr wallet) { - BOOST_CHECK(rescan_completed); - m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); - block_tx = TestSimpleSpend(*m_coinbase_txns[2], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); - m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); - mempool_tx = TestSimpleSpend(*m_coinbase_txns[3], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); - BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, false, error)); - SyncWithValidationInterfaceQueue(); - }); - wallet = TestLoadWallet(m_node.chain.get(), m_node.coinjoin_loader.get()); - BOOST_CHECK_EQUAL(addtx_count, 4); - { - LOCK(wallet->cs_wallet); - BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_tx.GetHash()), 1U); - BOOST_CHECK_EQUAL(wallet->mapWallet.count(mempool_tx.GetHash()), 1U); - } - - TestUnloadWallet(std::move(wallet)); -} - -// Explicit calculation which is used to test the wallet constant -// We get the same virtual size due to rounding(weight/4) for both use_max_sig values -static size_t CalculateNestedKeyhashInputSize(bool use_max_sig) -{ - // Generate ephemeral valid pubkey - CKey key; - key.MakeNewKey(true); - CPubKey pubkey = key.GetPubKey(); - - // Generate pubkey hash - uint160 key_hash(Hash160(pubkey)); - - // Create inner-script to enter into keystore. Key hash can't be 0... - CScript inner_script = CScript() << OP_0 << std::vector(key_hash.begin(), key_hash.end()); - - // Create outer P2SH script for the output - CScript script_pubkey = GetScriptForRawPubKey(pubkey); - - NodeContext node; - node.fee_estimator = std::make_unique(); - node.mempool = std::make_unique(node.fee_estimator.get()); - auto chain = interfaces::MakeChain(node); - CWallet wallet(chain.get(), /*coinjoin_loader=*/ nullptr, "", CreateDummyWalletDatabase()); - AddKey(wallet, key); - auto spk_man = wallet.GetLegacyScriptPubKeyMan(); - spk_man->AddCScript(inner_script); - - // Fill in dummy signatures for fee calculation. - SignatureData sig_data; - - if (!ProduceSignature(*spk_man, use_max_sig ? DUMMY_MAXIMUM_SIGNATURE_CREATOR : DUMMY_SIGNATURE_CREATOR, script_pubkey, sig_data)) { - // We're hand-feeding it correct arguments; shouldn't happen - assert(false); - } - - CTxIn tx_in; - UpdateInput(tx_in, sig_data); - return ::GetSerializeSize(tx_in, PROTOCOL_VERSION); -} - -BOOST_FIXTURE_TEST_CASE(dummy_input_size_test, TestChain100Setup) -{ - BOOST_CHECK_EQUAL(CalculateNestedKeyhashInputSize(false), DUMMY_NESTED_P2PKH_INPUT_SIZE); - BOOST_CHECK_EQUAL(CalculateNestedKeyhashInputSize(true), DUMMY_NESTED_P2PKH_INPUT_SIZE + 1); -} - -bool malformed_descriptor(std::ios_base::failure e) -{ - std::string s(e.what()); - return s.find("Missing checksum") != std::string::npos; -} - -BOOST_FIXTURE_TEST_CASE(wallet_descriptor_test, BasicTestingSetup) -{ - std::vector malformed_record; - CVectorWriter vw(0, 0, malformed_record, 0); - vw << std::string("notadescriptor"); - vw << (uint64_t)0; - vw << (int32_t)0; - vw << (int32_t)0; - vw << (int32_t)1; - - SpanReader vr{0, 0, malformed_record, 0}; - WalletDescriptor w_desc; - BOOST_CHECK_EXCEPTION(vr >> w_desc, std::ios_base::failure, malformed_descriptor); -} - -BOOST_FIXTURE_TEST_CASE(CreateWalletWithoutChain, BasicTestingSetup) -{ - // TODO: FIX FIX FIX - coinjoin_loader is null heere! - auto wallet = TestLoadWallet(nullptr, nullptr); - BOOST_CHECK(wallet); - UnloadWallet(std::move(wallet)); -} - -BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup) -{ - gArgs.ForceSetArg("-unsafesqlitesync", "1"); - auto chain = interfaces::MakeChain(m_node); - auto wallet = TestLoadWallet(m_node.chain.get(), m_node.coinjoin_loader.get()); - CKey key; - key.MakeNewKey(true); - AddKey(*wallet, key); - - bilingual_str error; - m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); - auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); - CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); - - SyncWithValidationInterfaceQueue(); - - { - auto block_hash = block_tx.GetHash(); - auto prev_hash = m_coinbase_txns[0]->GetHash(); - - LOCK(wallet->cs_wallet); - BOOST_CHECK(wallet->HasWalletSpend(prev_hash)); - BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 1u); - - std::vector vHashIn{ block_hash }, vHashOut; - BOOST_CHECK_EQUAL(wallet->ZapSelectTx(vHashIn, vHashOut), DBErrors::LOAD_OK); - - BOOST_CHECK(!wallet->HasWalletSpend(prev_hash)); - BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 0u); - } - - TestUnloadWallet(std::move(wallet)); -} - BOOST_AUTO_TEST_SUITE_END() From faec009a1ddb982e1885264d5da6ba94b5fa0418 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Wed, 8 Jan 2025 09:59:52 +0000 Subject: [PATCH 07/16] test: remove unneeded code from some `wallet_tests` This is done to avoid conflicts arising from backports. --- src/wallet/test/wallet_tests.cpp | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index 1e0b7e5d7f595..21ba6dd7ecd80 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -40,8 +40,6 @@ extern RPCHelpMan getrawchangeaddress(); extern RPCHelpMan getaddressinfo(); extern RPCHelpMan addmultisigaddress(); -extern RecursiveMutex cs_wallets; - // Ensure that fee levels defined in the wallet are at least as high // as the default levels for node policy. static_assert(DEFAULT_TRANSACTION_MINFEE >= DEFAULT_MIN_RELAY_TX_FEE, "wallet minimum fee is smaller than default relay fee"); @@ -605,11 +603,7 @@ BOOST_FIXTURE_TEST_CASE(ListCoins, ListCoinsTestingSetup) BOOST_FIXTURE_TEST_CASE(wallet_disableprivkeys, TestChain100Setup) { - NodeContext node; - node.fee_estimator = std::make_unique(); - node.mempool = std::make_unique(node.fee_estimator.get()); - auto chain = interfaces::MakeChain(node); - std::shared_ptr wallet = std::make_shared(chain.get(), m_node.coinjoin_loader.get(), "", CreateDummyWalletDatabase()); + std::shared_ptr wallet = std::make_shared(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateDummyWalletDatabase()); wallet->SetupLegacyScriptPubKeyMan(); wallet->SetMinVersion(FEATURE_LATEST); wallet->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS); @@ -637,19 +631,15 @@ static size_t CalculateNestedKeyhashInputSize(bool use_max_sig) // Create outer P2SH script for the output CScript script_pubkey = GetScriptForRawPubKey(pubkey); - NodeContext node; - node.fee_estimator = std::make_unique(); - node.mempool = std::make_unique(node.fee_estimator.get()); - auto chain = interfaces::MakeChain(node); - CWallet wallet(chain.get(), /*coinjoin_loader=*/ nullptr, "", CreateDummyWalletDatabase()); - AddKey(wallet, key); - auto spk_man = wallet.GetLegacyScriptPubKeyMan(); - spk_man->AddCScript(inner_script); + // Add inner-script to key store and key to watchonly + FillableSigningProvider keystore; + keystore.AddCScript(inner_script); + keystore.AddKeyPubKey(key, pubkey); // Fill in dummy signatures for fee calculation. SignatureData sig_data; - if (!ProduceSignature(*spk_man, use_max_sig ? DUMMY_MAXIMUM_SIGNATURE_CREATOR : DUMMY_SIGNATURE_CREATOR, script_pubkey, sig_data)) { + if (!ProduceSignature(keystore, use_max_sig ? DUMMY_MAXIMUM_SIGNATURE_CREATOR : DUMMY_SIGNATURE_CREATOR, script_pubkey, sig_data)) { // We're hand-feeding it correct arguments; shouldn't happen assert(false); } @@ -805,7 +795,6 @@ BOOST_FIXTURE_TEST_CASE(CreateWalletWithoutChain, BasicTestingSetup) BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup) { gArgs.ForceSetArg("-unsafesqlitesync", "1"); - auto chain = interfaces::MakeChain(m_node); auto wallet = TestLoadWallet(m_node.chain.get(), m_node.coinjoin_loader.get()); CKey key; key.MakeNewKey(true); From b649bd1dbbbb36d179525a14ad581b2999eac400 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Thu, 9 Jan 2025 08:34:35 +0000 Subject: [PATCH 08/16] partial bitcoin#19101: remove ::vpwallets and related global variables The PR is logically split in two for ease of review, this commit deals with folding ArgsManager, interfaces::Chain and interfaces::CoinJoin ::Loader into WalletContext. --- src/interfaces/wallet.h | 5 ++++- src/wallet/context.cpp | 5 ++--- src/wallet/context.h | 4 ++-- src/wallet/init.cpp | 2 +- src/wallet/interfaces.cpp | 27 ++++++++++++----------- src/wallet/load.cpp | 13 ++++++----- src/wallet/load.h | 7 +++--- src/wallet/rpcwallet.cpp | 6 ++--- src/wallet/test/init_test_fixture.cpp | 2 +- src/wallet/test/wallet_test_fixture.cpp | 2 +- src/wallet/test/wallet_tests.cpp | 25 ++++++++++++--------- src/wallet/wallet.cpp | 29 ++++++++++++++----------- src/wallet/wallet.h | 9 ++++---- 13 files changed, 76 insertions(+), 60 deletions(-) diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index 6de6776ef5d26..8d07b334bb55a 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -361,6 +361,9 @@ class WalletLoader : public ChainClient //! loaded at startup or by RPC. using LoadWalletFn = std::function wallet)>; virtual std::unique_ptr handleLoadWallet(LoadWalletFn fn) = 0; + + //! Return pointer to internal context, useful for testing. + virtual WalletContext* context() { return nullptr; } }; //! Information about one wallet address. @@ -448,7 +451,7 @@ std::unique_ptr MakeWallet(const std::shared_ptr& wallet); //! Return implementation of ChainClient interface for a wallet loader. This //! function will be undefined in builds where ENABLE_WALLET is false. -std::unique_ptr MakeWalletLoader(Chain& chain, const std::unique_ptr& coinjoin_loader, ArgsManager& args); +std::unique_ptr MakeWalletLoader(Chain& chain, ArgsManager& args, const std::unique_ptr& coinjoin_loader); } // namespace interfaces diff --git a/src/wallet/context.cpp b/src/wallet/context.cpp index d0cd59c8e5183..ec8dcb3884c95 100644 --- a/src/wallet/context.cpp +++ b/src/wallet/context.cpp @@ -4,7 +4,6 @@ #include -WalletContext::WalletContext(const std::unique_ptr& coinjoin_loader) : - m_coinjoin_loader(coinjoin_loader) -{} +WalletContext::WalletContext(const std::unique_ptr& coinjoin_loader) + : coinjoin_loader{coinjoin_loader} {} WalletContext::~WalletContext() {} diff --git a/src/wallet/context.h b/src/wallet/context.h index 059b7f1062395..cda736798035f 100644 --- a/src/wallet/context.h +++ b/src/wallet/context.h @@ -27,10 +27,10 @@ class Loader; //! behavior. struct WalletContext { interfaces::Chain* chain{nullptr}; - ArgsManager* args{nullptr}; + ArgsManager* args{nullptr}; // Currently a raw pointer because the memory is not managed by this struct // TODO: replace this unique_ptr to a pointer // probably possible to do after bitcoin/bitcoin#22219 - const std::unique_ptr& m_coinjoin_loader; + const std::unique_ptr& coinjoin_loader; //! Declare default constructor and destructor that are not inline, so code //! instantiating the WalletContext struct doesn't need to #include class diff --git a/src/wallet/init.cpp b/src/wallet/init.cpp index 8f7a416939261..7a920b5ec88f6 100644 --- a/src/wallet/init.cpp +++ b/src/wallet/init.cpp @@ -187,7 +187,7 @@ void WalletInit::Construct(NodeContext& node) const LogPrintf("Wallet disabled!\n"); return; } - auto wallet_loader = interfaces::MakeWalletLoader(*node.chain, node.coinjoin_loader, args); + auto wallet_loader = interfaces::MakeWalletLoader(*node.chain, args, node.coinjoin_loader); node.wallet_loader = wallet_loader.get(); node.chain_clients.emplace_back(std::move(wallet_loader)); } diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index c00924572abd7..cbb5d62c3c096 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -564,8 +564,8 @@ class WalletImpl : public Wallet class WalletLoaderImpl : public WalletLoader { public: - WalletLoaderImpl(Chain& chain, const std::unique_ptr& coinjoin_loader, ArgsManager& args) : - m_context(coinjoin_loader) + WalletLoaderImpl(Chain& chain, ArgsManager& args, const std::unique_ptr& coinjoin_loader) + : m_context{coinjoin_loader} { m_context.chain = &chain; m_context.args = &args; @@ -584,9 +584,9 @@ class WalletLoaderImpl : public WalletLoader m_rpc_handlers.emplace_back(m_context.chain->handleRpc(m_rpc_commands.back())); } } - bool verify() override { return VerifyWallets(*m_context.chain); } - bool load() override { assert(m_context.m_coinjoin_loader); return LoadWallets(*m_context.chain, *m_context.m_coinjoin_loader); } - void start(CScheduler& scheduler) override { return StartWallets(scheduler, *Assert(m_context.args)); } + bool verify() override { return VerifyWallets(m_context); } + bool load() override { assert(m_context.coinjoin_loader); return LoadWallets(m_context); } + void start(CScheduler& scheduler) override { return StartWallets(m_context, scheduler); } void flush() override { return FlushWallets(); } void stop() override { return StopWallets(); } void setMockTime(int64_t time) override { return SetMockTime(time); } @@ -600,22 +600,22 @@ class WalletLoaderImpl : public WalletLoader options.require_create = true; options.create_flags = wallet_creation_flags; options.create_passphrase = passphrase; - assert(m_context.m_coinjoin_loader); - return MakeWallet(CreateWallet(*m_context.chain, *m_context.m_coinjoin_loader, name, true /* load_on_start */, options, status, error, warnings)); + assert(m_context.coinjoin_loader); + return MakeWallet(CreateWallet(m_context, name, true /* load_on_start */, options, status, error, warnings)); } std::unique_ptr loadWallet(const std::string& name, bilingual_str& error, std::vector& warnings) override { DatabaseOptions options; DatabaseStatus status; options.require_existing = true; - assert(m_context.m_coinjoin_loader); - return MakeWallet(LoadWallet(*m_context.chain, *m_context.m_coinjoin_loader, name, true /* load_on_start */, options, status, error, warnings)); + assert(m_context.coinjoin_loader); + return MakeWallet(LoadWallet(m_context, name, true /* load_on_start */, options, status, error, warnings)); } std::unique_ptr restoreWallet(const fs::path& backup_file, const std::string& wallet_name, bilingual_str& error, std::vector& warnings) override { DatabaseStatus status; - assert(m_context.m_coinjoin_loader); - return MakeWallet(RestoreWallet(*m_context.chain, *m_context.m_coinjoin_loader, backup_file, wallet_name, /*load_on_start=*/true, status, error, warnings)); + assert(m_context.coinjoin_loader); + return MakeWallet(RestoreWallet(m_context, backup_file, wallet_name, /*load_on_start=*/true, status, error, warnings)); } std::string getWalletDir() override { @@ -641,6 +641,7 @@ class WalletLoaderImpl : public WalletLoader { return HandleLoadWallet(std::move(fn)); } + WalletContext* context() override { return &m_context; } WalletContext m_context; const std::vector m_wallet_filenames; @@ -652,7 +653,7 @@ class WalletLoaderImpl : public WalletLoader namespace interfaces { std::unique_ptr MakeWallet(const std::shared_ptr& wallet) { return wallet ? std::make_unique(wallet) : nullptr; } -std::unique_ptr MakeWalletLoader(Chain& chain, const std::unique_ptr& coinjoin_loader, ArgsManager& args) { - return std::make_unique(chain, coinjoin_loader, args); +std::unique_ptr MakeWalletLoader(Chain& chain, ArgsManager& args, const std::unique_ptr& coinjoin_loader) { + return std::make_unique(chain, args, coinjoin_loader); } } // namespace interfaces diff --git a/src/wallet/load.cpp b/src/wallet/load.cpp index 6f5b661efd728..8696396c159de 100644 --- a/src/wallet/load.cpp +++ b/src/wallet/load.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -21,8 +22,9 @@ #include -bool VerifyWallets(interfaces::Chain& chain) +bool VerifyWallets(WalletContext& context) { + interfaces::Chain& chain = *context.chain; if (gArgs.IsArgSet("-walletdir")) { const fs::path wallet_dir{gArgs.GetPathArg("-walletdir")}; std::error_code error; @@ -96,8 +98,9 @@ bool VerifyWallets(interfaces::Chain& chain) return true; } -bool LoadWallets(interfaces::Chain& chain, interfaces::CoinJoin::Loader& coinjoin_loader) +bool LoadWallets(WalletContext& context) { + interfaces::Chain& chain = *context.chain; try { std::set wallet_paths; for (const auto& wallet : chain.getSettingsList("wallet")) { @@ -116,7 +119,7 @@ bool LoadWallets(interfaces::Chain& chain, interfaces::CoinJoin::Loader& coinjoi continue; } chain.initMessage(_("Loading wallet…").translated); - std::shared_ptr pwallet = database ? CWallet::Create(&chain, &coinjoin_loader, name, std::move(database), options.create_flags, error_string, warnings) : nullptr; + std::shared_ptr pwallet = database ? CWallet::Create(context, name, std::move(database), options.create_flags, error_string, warnings) : nullptr; if (!warnings.empty()) chain.initWarning(Join(warnings, Untranslated("\n"))); if (!pwallet) { chain.initError(error_string); @@ -131,14 +134,14 @@ bool LoadWallets(interfaces::Chain& chain, interfaces::CoinJoin::Loader& coinjoi } } -void StartWallets(CScheduler& scheduler, const ArgsManager& args) +void StartWallets(WalletContext& context, CScheduler& scheduler) { for (const std::shared_ptr& pwallet : GetWallets()) { pwallet->postInitProcess(); } // Schedule periodic wallet flushes and tx rebroadcasts - if (args.GetBoolArg("-flushwallet", DEFAULT_FLUSHWALLET)) { + if (context.args->GetBoolArg("-flushwallet", DEFAULT_FLUSHWALLET)) { scheduler.scheduleEvery(MaybeCompactWalletDB, std::chrono::milliseconds{500}); } scheduler.scheduleEvery(MaybeResendWalletTxs, std::chrono::milliseconds{1000}); diff --git a/src/wallet/load.h b/src/wallet/load.h index cb7eb77b1b3c2..0c02d8d71ace5 100644 --- a/src/wallet/load.h +++ b/src/wallet/load.h @@ -12,6 +12,7 @@ class ArgsManager; class CConnman; class CScheduler; +struct WalletContext; namespace interfaces { class Chain; @@ -21,13 +22,13 @@ class Loader; } // namespace interfaces //! Responsible for reading and validating the -wallet arguments and verifying the wallet database. -bool VerifyWallets(interfaces::Chain& chain); +bool VerifyWallets(WalletContext& context); //! Load wallet databases. -bool LoadWallets(interfaces::Chain& chain, interfaces::CoinJoin::Loader& coinjoin_loader); +bool LoadWallets(WalletContext& context); //! Complete startup of wallets. -void StartWallets(CScheduler& scheduler, const ArgsManager& args); +void StartWallets(WalletContext& context, CScheduler& scheduler); //! Flush all wallets in preparation for shutdown. void FlushWallets(); diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 0e9c5aa778c0e..e5c2cc76156d7 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -2909,7 +2909,7 @@ static RPCHelpMan loadwallet() bilingual_str error; std::vector warnings; std::optional load_on_start = request.params[1].isNull() ? std::nullopt : std::optional(request.params[1].get_bool()); - std::shared_ptr const wallet = LoadWallet(*context.chain, *context.m_coinjoin_loader, name, load_on_start, options, status, error, warnings); + std::shared_ptr const wallet = LoadWallet(context, name, load_on_start, options, status, error, warnings); HandleWalletError(wallet, status, error); @@ -3065,7 +3065,7 @@ static RPCHelpMan createwallet() options.create_passphrase = passphrase; bilingual_str error; std::optional load_on_start = request.params[6].isNull() ? std::nullopt : std::optional(request.params[6].get_bool()); - std::shared_ptr wallet = CreateWallet(*context.chain, *context.m_coinjoin_loader, request.params[0].get_str(), load_on_start, options, status, error, warnings); + std::shared_ptr wallet = CreateWallet(context, request.params[0].get_str(), load_on_start, options, status, error, warnings); if (!wallet) { RPCErrorCode code = status == DatabaseStatus::FAILED_ENCRYPT ? RPC_WALLET_ENCRYPTION_FAILED : RPC_WALLET_ERROR; throw JSONRPCError(code, error.original); @@ -3119,7 +3119,7 @@ static RPCHelpMan restorewallet() bilingual_str error; std::vector warnings; - const std::shared_ptr wallet = RestoreWallet(*context.chain, *context.m_coinjoin_loader, backup_file, wallet_name, load_on_start, status, error, warnings); + const std::shared_ptr wallet = RestoreWallet(context, backup_file, wallet_name, load_on_start, status, error, warnings); HandleWalletError(wallet, status, error); diff --git a/src/wallet/test/init_test_fixture.cpp b/src/wallet/test/init_test_fixture.cpp index 55d3e0b5d8028..88df028e6fa8a 100644 --- a/src/wallet/test/init_test_fixture.cpp +++ b/src/wallet/test/init_test_fixture.cpp @@ -14,7 +14,7 @@ InitWalletDirTestingSetup::InitWalletDirTestingSetup(const std::string& chainName) : BasicTestingSetup(chainName) { - m_wallet_loader = MakeWalletLoader(*m_node.chain, m_node.coinjoin_loader, *Assert(m_node.args)); + m_wallet_loader = MakeWalletLoader(*m_node.chain, *Assert(m_node.args), m_node.coinjoin_loader); std::string sep; sep += fs::path::preferred_separator; diff --git a/src/wallet/test/wallet_test_fixture.cpp b/src/wallet/test/wallet_test_fixture.cpp index 93f635da79a9c..a72cd7b667bca 100644 --- a/src/wallet/test/wallet_test_fixture.cpp +++ b/src/wallet/test/wallet_test_fixture.cpp @@ -8,7 +8,7 @@ WalletTestingSetup::WalletTestingSetup(const std::string& chainName) : TestingSetup(chainName), - m_wallet_loader{interfaces::MakeWalletLoader(*m_node.chain, m_node.coinjoin_loader, *Assert(m_node.args))}, + m_wallet_loader{interfaces::MakeWalletLoader(*m_node.chain, *Assert(m_node.args), m_node.coinjoin_loader)}, m_wallet(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateMockWalletDatabase()) { m_wallet.LoadWallet(); diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index 21ba6dd7ecd80..9f693dc6994c4 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -47,19 +48,19 @@ static_assert(WALLET_INCREMENTAL_RELAY_FEE >= DEFAULT_INCREMENTAL_RELAY_FEE, "wa BOOST_FIXTURE_TEST_SUITE(wallet_tests, WalletTestingSetup) -static std::shared_ptr TestLoadWallet(interfaces::Chain* chain, interfaces::CoinJoin::Loader* coinjoin_loader) +static std::shared_ptr TestLoadWallet(WalletContext& context) { DatabaseOptions options; DatabaseStatus status; bilingual_str error; std::vector warnings; auto database = MakeWalletDatabase("", options, status, error); - auto wallet = CWallet::Create(chain, coinjoin_loader, "", std::move(database), options.create_flags, error, warnings); - if (coinjoin_loader) { + auto wallet = CWallet::Create(context, "", std::move(database), options.create_flags, error, warnings); + if (context.coinjoin_loader) { // TODO: see CreateWalletWithoutChain AddWallet(wallet); } - if (chain) { + if (context.chain) { wallet->postInitProcess(); } return wallet; @@ -698,7 +699,9 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup) { gArgs.ForceSetArg("-unsafesqlitesync", "1"); // Create new wallet with known key and unload it. - auto wallet = TestLoadWallet(m_node.chain.get(), m_node.coinjoin_loader.get()); + WalletContext context{m_node.coinjoin_loader}; + context.chain = m_node.chain.get(); + auto wallet = TestLoadWallet(context); CKey key; key.MakeNewKey(true); AddKey(*wallet, key); @@ -738,7 +741,7 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup) // Reload wallet and make sure new transactions are detected despite events // being blocked - wallet = TestLoadWallet(m_node.chain.get(), m_node.coinjoin_loader.get()); + wallet = TestLoadWallet(context); BOOST_CHECK(rescan_completed); BOOST_CHECK_EQUAL(addtx_count, 2); { @@ -773,7 +776,7 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup) BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, false, error)); SyncWithValidationInterfaceQueue(); }); - wallet = TestLoadWallet(m_node.chain.get(), m_node.coinjoin_loader.get()); + wallet = TestLoadWallet(context); BOOST_CHECK_EQUAL(addtx_count, 4); { LOCK(wallet->cs_wallet); @@ -786,8 +789,8 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup) BOOST_FIXTURE_TEST_CASE(CreateWalletWithoutChain, BasicTestingSetup) { - // TODO: FIX FIX FIX - coinjoin_loader is null heere! - auto wallet = TestLoadWallet(nullptr, nullptr); + WalletContext context{/*coinjoin_loader=*/nullptr}; // TODO: FIX FIX FIX + auto wallet = TestLoadWallet(context); BOOST_CHECK(wallet); UnloadWallet(std::move(wallet)); } @@ -795,7 +798,9 @@ BOOST_FIXTURE_TEST_CASE(CreateWalletWithoutChain, BasicTestingSetup) BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup) { gArgs.ForceSetArg("-unsafesqlitesync", "1"); - auto wallet = TestLoadWallet(m_node.chain.get(), m_node.coinjoin_loader.get()); + WalletContext context{m_node.coinjoin_loader}; + context.chain = m_node.chain.get(); + auto wallet = TestLoadWallet(context); CKey key; key.MakeNewKey(true); AddKey(*wallet, key); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index d607311a470c6..b762dad809f2e 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -40,6 +40,7 @@ #endif #include #include +#include #include #include @@ -229,7 +230,7 @@ void UnloadWallet(std::shared_ptr&& wallet) } namespace { -std::shared_ptr LoadWalletInternal(interfaces::Chain& chain, interfaces::CoinJoin::Loader& coinjoin_loader, const std::string& name, std::optional load_on_start, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector& warnings) +std::shared_ptr LoadWalletInternal(WalletContext& context, const std::string& name, std::optional load_on_start, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector& warnings) { try { std::unique_ptr database = MakeWalletDatabase(name, options, status, error); @@ -238,8 +239,8 @@ std::shared_ptr LoadWalletInternal(interfaces::Chain& chain, interfaces return nullptr; } - chain.initMessage(_("Loading wallet…").translated); - std::shared_ptr wallet = CWallet::Create(&chain, &coinjoin_loader, name, std::move(database), options.create_flags, error, warnings); + context.chain->initMessage(_("Loading wallet…").translated); + std::shared_ptr wallet = CWallet::Create(context, name, std::move(database), options.create_flags, error, warnings); if (!wallet) { error = Untranslated("Wallet loading failed.") + Untranslated(" ") + error; status = DatabaseStatus::FAILED_LOAD; @@ -249,7 +250,7 @@ std::shared_ptr LoadWalletInternal(interfaces::Chain& chain, interfaces wallet->postInitProcess(); // Write the wallet setting - UpdateWalletSetting(chain, name, load_on_start, warnings); + UpdateWalletSetting(*context.chain, name, load_on_start, warnings); return wallet; } catch (const std::runtime_error& e) { @@ -260,7 +261,7 @@ std::shared_ptr LoadWalletInternal(interfaces::Chain& chain, interfaces } } // namespace -std::shared_ptr LoadWallet(interfaces::Chain& chain, interfaces::CoinJoin::Loader& coinjoin_loader, const std::string& name, std::optional load_on_start, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector& warnings) +std::shared_ptr LoadWallet(WalletContext& context, const std::string& name, std::optional load_on_start, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector& warnings) { auto result = WITH_LOCK(g_loading_wallet_mutex, return g_loading_wallet_set.insert(name)); if (!result.second) { @@ -268,12 +269,12 @@ std::shared_ptr LoadWallet(interfaces::Chain& chain, interfaces::CoinJo status = DatabaseStatus::FAILED_LOAD; return nullptr; } - auto wallet = LoadWalletInternal(chain, coinjoin_loader, name, load_on_start, options, status, error, warnings); + auto wallet = LoadWalletInternal(context, name, load_on_start, options, status, error, warnings); WITH_LOCK(g_loading_wallet_mutex, g_loading_wallet_set.erase(result.first)); return wallet; } -std::shared_ptr CreateWallet(interfaces::Chain& chain, interfaces::CoinJoin::Loader& coinjoin_loader, const std::string& name, std::optional load_on_start, DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector& warnings) +std::shared_ptr CreateWallet(WalletContext& context, const std::string& name, std::optional load_on_start, DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector& warnings) { uint64_t wallet_creation_flags = options.create_flags; const SecureString& passphrase = options.create_passphrase; @@ -304,8 +305,8 @@ std::shared_ptr CreateWallet(interfaces::Chain& chain, interfaces::Coin } // Make the wallet - chain.initMessage(_("Loading wallet…").translated); - std::shared_ptr wallet = CWallet::Create(&chain, &coinjoin_loader, name, std::move(database), wallet_creation_flags, error, warnings); + context.chain->initMessage(_("Loading wallet…").translated); + std::shared_ptr wallet = CWallet::Create(context, name, std::move(database), wallet_creation_flags, error, warnings); if (!wallet) { error = Untranslated("Wallet creation failed.") + Untranslated(" ") + error; status = DatabaseStatus::FAILED_CREATE; @@ -361,13 +362,13 @@ std::shared_ptr CreateWallet(interfaces::Chain& chain, interfaces::Coin wallet->postInitProcess(); // Write the wallet settings - UpdateWalletSetting(chain, name, load_on_start, warnings); + UpdateWalletSetting(*context.chain, name, load_on_start, warnings); status = DatabaseStatus::SUCCESS; return wallet; } -std::shared_ptr RestoreWallet(interfaces::Chain& chain, interfaces::CoinJoin::Loader& coinjoin_loader, const fs::path& backup_file, const std::string& wallet_name, std::optional load_on_start, DatabaseStatus& status, bilingual_str& error, std::vector& warnings) +std::shared_ptr RestoreWallet(WalletContext& context, const fs::path& backup_file, const std::string& wallet_name, std::optional load_on_start, DatabaseStatus& status, bilingual_str& error, std::vector& warnings) { DatabaseOptions options; options.require_existing = true; @@ -389,7 +390,7 @@ std::shared_ptr RestoreWallet(interfaces::Chain& chain, interfaces::Coi auto wallet_file = wallet_path / "wallet.dat"; fs::copy_file(backup_file, wallet_file, fs::copy_options::none); - auto wallet = LoadWallet(chain, coinjoin_loader, wallet_name, load_on_start, options, status, error, warnings); + auto wallet = LoadWallet(context, wallet_name, load_on_start, options, status, error, warnings); if (!wallet) { fs::remove(wallet_file); @@ -3266,8 +3267,10 @@ std::unique_ptr MakeWalletDatabase(const std::string& name, cons return MakeDatabase(wallet_path, options, status, error_string); } -std::shared_ptr CWallet::Create(interfaces::Chain* chain, interfaces::CoinJoin::Loader* coinjoin_loader, const std::string& name, std::unique_ptr database, uint64_t wallet_creation_flags, bilingual_str& error, std::vector& warnings) +std::shared_ptr CWallet::Create(WalletContext& context, const std::string& name, std::unique_ptr database, uint64_t wallet_creation_flags, bilingual_str& error, std::vector& warnings) { + interfaces::Chain* chain = context.chain; + interfaces::CoinJoin::Loader* coinjoin_loader = context.coinjoin_loader.get(); const std::string& walletFile = database->Filename(); const auto start{SteadyClock::now()}; diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 5a1ca8b49b357..59d122d0cc8bb 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -49,6 +49,7 @@ #include struct bilingual_str; +struct WalletContext; using LoadWalletFn = std::function wallet)>; @@ -64,9 +65,9 @@ bool RemoveWallet(const std::shared_ptr& wallet, std::optional lo bool RemoveWallet(const std::shared_ptr& wallet, std::optional load_on_start); std::vector> GetWallets(); std::shared_ptr GetWallet(const std::string& name); -std::shared_ptr LoadWallet(interfaces::Chain& chain, interfaces::CoinJoin::Loader& coinjoin_loader, const std::string& name, std::optional load_on_start, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector& warnings); -std::shared_ptr CreateWallet(interfaces::Chain& chain, interfaces::CoinJoin::Loader& coinjoin_loader, const std::string& name, std::optional load_on_start, DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector& warnings); -std::shared_ptr RestoreWallet(interfaces::Chain& chain, interfaces::CoinJoin::Loader& coinjoin_loader, const fs::path& backup_file, const std::string& wallet_name, std::optional load_on_start, DatabaseStatus& status, bilingual_str& error, std::vector& warnings); +std::shared_ptr LoadWallet(WalletContext& context, const std::string& name, std::optional load_on_start, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector& warnings); +std::shared_ptr CreateWallet(WalletContext& context, const std::string& name, std::optional load_on_start, DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector& warnings); +std::shared_ptr RestoreWallet(WalletContext& context, const fs::path& backup_file, const std::string& wallet_name, std::optional load_on_start, DatabaseStatus& status, bilingual_str& error, std::vector& warnings); std::unique_ptr HandleLoadWallet(LoadWalletFn load_wallet); std::unique_ptr MakeWalletDatabase(const std::string& name, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error); @@ -900,7 +901,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati bool ResendTransaction(const uint256& hashTx); /* Initializes the wallet, returns a new CWallet instance or a null pointer in case of an error */ - static std::shared_ptr Create(interfaces::Chain* chain, interfaces::CoinJoin::Loader* coinjoin_loader, const std::string& name, std::unique_ptr database, uint64_t wallet_creation_flags, bilingual_str& error, std::vector& warnings); + static std::shared_ptr Create(WalletContext& context, const std::string& name, std::unique_ptr database, uint64_t wallet_creation_flags, bilingual_str& error, std::vector& warnings); /** * Wallet post-init setup From fefa5cd678e1c99a30b8d75555726cce81cc14f6 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 12 Jan 2025 10:33:56 +0000 Subject: [PATCH 09/16] refactor: use `WalletLoader` to access wallets in `WalletInit` funcs This is required because we're going to get deglobalize wallet variables in an upcoming commit, which requires us to hold `WalletContext` in order to iterate through all wallets. So, we rely on the interface to help us do that. Also, let's fold in the `WalletInit::InitCoinJoinSettings` calls into `interfaces::CoinJoin::Loader` for simplicity's sake. --- src/coinjoin/client.cpp | 20 +++++++------------- src/coinjoin/interfaces.cpp | 23 +++++++++++++++++++---- src/dummywallet.cpp | 5 +++-- src/init.cpp | 12 +++++++++--- src/interfaces/coinjoin.h | 5 ++++- src/interfaces/wallet.h | 3 +++ src/node/blockstorage.cpp | 2 -- src/test/util/setup_common.cpp | 2 +- src/wallet/init.cpp | 21 +++++++++++---------- src/wallet/interfaces.cpp | 1 + src/walletinitinterface.h | 9 ++++++--- 11 files changed, 64 insertions(+), 39 deletions(-) diff --git a/src/coinjoin/client.cpp b/src/coinjoin/client.cpp index 4858223e07cce..0562fc9b312c9 100644 --- a/src/coinjoin/client.cpp +++ b/src/coinjoin/client.cpp @@ -1914,14 +1914,11 @@ void CCoinJoinClientManager::GetJsonInfo(UniValue& obj) const void CoinJoinWalletManager::Add(const std::shared_ptr& wallet) { - { - LOCK(cs_wallet_manager_map); - m_wallet_manager_map.try_emplace(wallet->GetName(), - std::make_unique(wallet, *this, m_dmnman, m_mn_metaman, - m_mn_sync, m_isman, m_queueman, - m_is_masternode)); - } - g_wallet_init_interface.InitCoinJoinSettings(*this); + LOCK(cs_wallet_manager_map); + m_wallet_manager_map.try_emplace(wallet->GetName(), + std::make_unique(wallet, *this, m_dmnman, m_mn_metaman, + m_mn_sync, m_isman, m_queueman, + m_is_masternode)); } void CoinJoinWalletManager::DoMaintenance(CConnman& connman) @@ -1933,11 +1930,8 @@ void CoinJoinWalletManager::DoMaintenance(CConnman& connman) } void CoinJoinWalletManager::Remove(const std::string& name) { - { - LOCK(cs_wallet_manager_map); - m_wallet_manager_map.erase(name); - } - g_wallet_init_interface.InitCoinJoinSettings(*this); + LOCK(cs_wallet_manager_map); + m_wallet_manager_map.erase(name); } void CoinJoinWalletManager::Flush(const std::string& name) diff --git a/src/coinjoin/interfaces.cpp b/src/coinjoin/interfaces.cpp index 6c40a3b6d3e07..d48fea8dfa3dc 100644 --- a/src/coinjoin/interfaces.cpp +++ b/src/coinjoin/interfaces.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -62,15 +63,25 @@ class CoinJoinClientImpl : public interfaces::CoinJoin::Client class CoinJoinLoaderImpl : public interfaces::CoinJoin::Loader { CoinJoinWalletManager& m_walletman; + interfaces::WalletLoader& m_wallet_loader; public: - explicit CoinJoinLoaderImpl(CoinJoinWalletManager& walletman) - : m_walletman(walletman) {} + explicit CoinJoinLoaderImpl(CoinJoinWalletManager& walletman, interfaces::WalletLoader& wallet_loader) : + m_walletman{walletman}, + m_wallet_loader{wallet_loader} + { + g_wallet_init_interface.InitCoinJoinSettings(m_wallet_loader, m_walletman); + } - void AddWallet(const std::shared_ptr& wallet) override { m_walletman.Add(wallet); } + void AddWallet(const std::shared_ptr& wallet) override + { + m_walletman.Add(wallet); + g_wallet_init_interface.InitCoinJoinSettings(m_wallet_loader, m_walletman); + } void RemoveWallet(const std::string& name) override { m_walletman.Remove(name); + g_wallet_init_interface.InitCoinJoinSettings(m_wallet_loader, m_walletman); } void FlushWallet(const std::string& name) override { @@ -91,5 +102,9 @@ class CoinJoinLoaderImpl : public interfaces::CoinJoin::Loader } // namespace coinjoin namespace interfaces { -std::unique_ptr MakeCoinJoinLoader(CoinJoinWalletManager& walletman) { return std::make_unique(walletman); } +std::unique_ptr MakeCoinJoinLoader(CoinJoinWalletManager& walletman, + interfaces::WalletLoader& wallet_loader) +{ + return std::make_unique(walletman, wallet_loader); +} } // namespace interfaces diff --git a/src/dummywallet.cpp b/src/dummywallet.cpp index a9e5ae9486a8f..6e5c59b10bafc 100644 --- a/src/dummywallet.cpp +++ b/src/dummywallet.cpp @@ -12,6 +12,7 @@ namespace interfaces { class Chain; class Handler; class Wallet; +class WalletLoader; } class DummyWalletInit : public WalletInitInterface { @@ -23,8 +24,8 @@ class DummyWalletInit : public WalletInitInterface { void Construct(NodeContext& node) const override {LogPrintf("No wallet support compiled in!\n");} // Dash Specific WalletInitInterface InitCoinJoinSettings - void AutoLockMasternodeCollaterals() const override {} - void InitCoinJoinSettings(const CoinJoinWalletManager& cjwalletman) const override {} + void AutoLockMasternodeCollaterals(interfaces::WalletLoader& wallet_loader) const override {} + void InitCoinJoinSettings(interfaces::WalletLoader& wallet_loader, const CoinJoinWalletManager& cjwalletman) const override {} bool InitAutoBackup() const override {return true;} }; diff --git a/src/init.cpp b/src/init.cpp index 2accb1ad92b0c..151db2a338dd2 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -25,10 +25,10 @@ #include #include #include -#include #include #include #include +#include #include #include #include @@ -2028,8 +2028,9 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) !ignores_incoming_txs); #ifdef ENABLE_WALLET - node.coinjoin_loader = interfaces::MakeCoinJoinLoader(*node.cj_ctx->walletman); - g_wallet_init_interface.InitCoinJoinSettings(*node.cj_ctx->walletman); + if (!args.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) { + node.coinjoin_loader = interfaces::MakeCoinJoinLoader(*node.cj_ctx->walletman, *node.wallet_loader); + } #endif // ENABLE_WALLET // ********************************************************* Step 7d: Setup other Dash services @@ -2206,6 +2207,11 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) chainman.m_load_block = std::thread(&util::TraceThread, "loadblk", [=, &args, &chainman, &node] { ThreadImport(chainman, *node.dmnman, *g_ds_notification_interface, vImportFiles, node.mn_activeman.get(), args); }); +#ifdef ENABLE_WALLET + if (!args.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) { + g_wallet_init_interface.AutoLockMasternodeCollaterals(*node.wallet_loader); + } +#endif // ENABLE_WALLET // Wait for genesis block to be processed { diff --git a/src/interfaces/coinjoin.h b/src/interfaces/coinjoin.h index 7f881bf07f5c3..4a4a43be13092 100644 --- a/src/interfaces/coinjoin.h +++ b/src/interfaces/coinjoin.h @@ -10,6 +10,9 @@ class CoinJoinWalletManager; class CWallet; +namespace interfaces { +class WalletLoader; +} namespace interfaces { namespace CoinJoin { @@ -42,7 +45,7 @@ class Loader }; } // namespace CoinJoin -std::unique_ptr MakeCoinJoinLoader(CoinJoinWalletManager& walletman); +std::unique_ptr MakeCoinJoinLoader(CoinJoinWalletManager& walletman, interfaces::WalletLoader& wallet_loader); } // namespace interfaces diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index 8d07b334bb55a..1401dca807559 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -86,6 +86,9 @@ class Wallet //! Abort a rescan. virtual void abortRescan() = 0; + //! Lock masternode collaterals + virtual void autoLockMasternodeCollaterals() = 0; + //! Back up wallet. virtual bool backupWallet(const std::string& filename) = 0; diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index 0f00280734981..2034846086043 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -926,7 +926,5 @@ void ThreadImport(ChainstateManager& chainman, CDeterministicMNManager& dmnman, mn_activeman->Init(chainman.ActiveTip()); } - g_wallet_init_interface.AutoLockMasternodeCollaterals(); - chainman.ActiveChainstate().LoadMempool(args); } diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp index a1800745f193d..c6031f81159a8 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -124,7 +124,7 @@ void DashPostChainstateSetup(NodeContext& node) /*mn_activeman=*/nullptr, *node.mn_sync, *node.llmq_ctx->isman, node.peerman, /*relay_txes=*/true); #ifdef ENABLE_WALLET - node.coinjoin_loader = interfaces::MakeCoinJoinLoader(*node.cj_ctx->walletman); + node.coinjoin_loader = interfaces::MakeCoinJoinLoader(*node.cj_ctx->walletman, *node.wallet_loader); #endif // ENABLE_WALLET } diff --git a/src/wallet/init.cpp b/src/wallet/init.cpp index 7a920b5ec88f6..206c628db81bc 100644 --- a/src/wallet/init.cpp +++ b/src/wallet/init.cpp @@ -45,8 +45,8 @@ class WalletInit : public WalletInitInterface void Construct(NodeContext& node) const override; // Dash Specific Wallet Init - void AutoLockMasternodeCollaterals() const override; - void InitCoinJoinSettings(const CoinJoinWalletManager& cjwalletman) const override; + void AutoLockMasternodeCollaterals(interfaces::WalletLoader& wallet_loader) const override; + void InitCoinJoinSettings(interfaces::WalletLoader& wallet_loader, const CoinJoinWalletManager& cjwalletman) const override; bool InitAutoBackup() const override; }; @@ -193,25 +193,26 @@ void WalletInit::Construct(NodeContext& node) const } -void WalletInit::AutoLockMasternodeCollaterals() const +void WalletInit::AutoLockMasternodeCollaterals(interfaces::WalletLoader& wallet_loader) const { // we can't do this before DIP3 is fully initialized - for (const auto& pwallet : GetWallets()) { - pwallet->AutoLockMasternodeCollaterals(); + for (const auto& wallet : wallet_loader.getWallets()) { + wallet->autoLockMasternodeCollaterals(); } } -void WalletInit::InitCoinJoinSettings(const CoinJoinWalletManager& cjwalletman) const +void WalletInit::InitCoinJoinSettings(interfaces::WalletLoader& wallet_loader, const CoinJoinWalletManager& cjwalletman) const { - CCoinJoinClientOptions::SetEnabled(!GetWallets().empty() ? gArgs.GetBoolArg("-enablecoinjoin", true) : false); + const auto& wallets{wallet_loader.getWallets()}; + CCoinJoinClientOptions::SetEnabled(!wallets.empty() ? gArgs.GetBoolArg("-enablecoinjoin", true) : false); if (!CCoinJoinClientOptions::IsEnabled()) { return; } bool fAutoStart = gArgs.GetBoolArg("-coinjoinautostart", DEFAULT_COINJOIN_AUTOSTART); - for (auto& pwallet : GetWallets()) { - auto manager = cjwalletman.Get(pwallet->GetName()); + for (auto& wallet : wallets) { + auto manager = cjwalletman.Get(wallet->getWalletName()); assert(manager != nullptr); - if (pwallet->IsLocked()) { + if (wallet->isLocked(/*fForMixing=*/false)) { manager->StopMixing(); } else if (fAutoStart) { manager->StartMixing(); diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index cbb5d62c3c096..aa1930838f51f 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -147,6 +147,7 @@ class WalletImpl : public Wallet return m_wallet->ChangeWalletPassphrase(old_wallet_passphrase, new_wallet_passphrase); } void abortRescan() override { m_wallet->AbortRescan(); } + void autoLockMasternodeCollaterals() override { m_wallet->AutoLockMasternodeCollaterals(); } bool backupWallet(const std::string& filename) override { return m_wallet->BackupWallet(filename); } bool autoBackupWallet(const fs::path& wallet_path, bilingual_str& error_string, std::vector& warnings) override { diff --git a/src/walletinitinterface.h b/src/walletinitinterface.h index cb70b76bc649e..684ec6c338c39 100644 --- a/src/walletinitinterface.h +++ b/src/walletinitinterface.h @@ -7,9 +7,12 @@ class ArgsManager; class CoinJoinWalletManager; - struct NodeContext; +namespace interfaces { +class WalletLoader; +}; + class WalletInitInterface { public: /** Is the wallet component enabled */ @@ -22,8 +25,8 @@ class WalletInitInterface { virtual void Construct(NodeContext& node) const = 0; // Dash Specific WalletInitInterface - virtual void AutoLockMasternodeCollaterals() const = 0; - virtual void InitCoinJoinSettings(const CoinJoinWalletManager& cjwalletman) const = 0; + virtual void AutoLockMasternodeCollaterals(interfaces::WalletLoader& wallet_loader) const = 0; + virtual void InitCoinJoinSettings(interfaces::WalletLoader& wallet_loader, const CoinJoinWalletManager& cjwalletman) const = 0; virtual bool InitAutoBackup() const = 0; virtual ~WalletInitInterface() {} From 8d4aae2543cfdf05965d358b65b0499265540857 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Mon, 13 Jan 2025 00:50:35 +0000 Subject: [PATCH 10/16] refactor: separate out Dash-specific RPCs that rely on wallet logic In an upcoming commit, wallet variables will be deglobalized. This means that RPCs that use wallet logic need to get ahold of WalletContext, which only happens if they're registered as a wallet RPC (i.e. registered through WalletLoader). The downside of being registered as a wallet RPC is that you lose access to NodeContext. For now, we will work around this by giving WalletContext access to NodeContext and modify EnsureAnyNodeContext to pull it from WalletContext. --- src/init.cpp | 18 +++++++++ src/interfaces/wallet.h | 16 ++++++-- src/rpc/coinjoin.cpp | 45 +++++++++++++++++----- src/rpc/evo.cpp | 50 ++++++++++++++++++++----- src/rpc/governance.cpp | 23 +++++++++--- src/rpc/masternode.cpp | 17 +++++++-- src/rpc/register.h | 12 ++++++ src/rpc/server_util.cpp | 20 ++++++++-- src/wallet/context.h | 7 ++++ src/wallet/init.cpp | 2 +- src/wallet/interfaces.cpp | 37 ++++++++++++------ src/wallet/test/init_test_fixture.cpp | 2 +- src/wallet/test/wallet_test_fixture.cpp | 2 +- 13 files changed, 201 insertions(+), 50 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index 151db2a338dd2..28d0300adb98a 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -1512,6 +1513,23 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) for (const auto& client : node.chain_clients) { client->registerRpcs(); } +#ifdef ENABLE_WALLET + // Register non-core wallet-only RPC commands. These are commands that + // aren't a part of the wallet library but heavily rely on wallet logic. + // TODO: Move them to chain client interfaces so they can be called + // with registerRpcs() + if (!args.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) { + for (const auto& commands : { + GetWalletCoinJoinRPCCommands(), + GetWalletEvoRPCCommands(), + GetWalletGovernanceRPCCommands(), + GetWalletMasternodeRPCCommands(), + }) { + node.wallet_loader->registerOtherRpcs(commands); + } + } +#endif // ENABLE_WALLET + #if ENABLE_ZMQ RegisterZMQRPCCommands(tableRPC); #endif diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index 1401dca807559..b0bb51c7cd298 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -28,18 +28,22 @@ class CCoinControl; class CFeeRate; class CKey; +class CRPCCommand; class CWallet; class UniValue; enum class FeeReason; enum class TransactionError; enum isminetype : unsigned int; -struct bilingual_str; struct CRecipient; +struct NodeContext; struct PartiallySignedTransaction; struct WalletContext; struct bilingual_str; using isminefilter = std::underlying_type::type; +template +class Span; + namespace interfaces { class Handler; @@ -341,8 +345,11 @@ class Wallet class WalletLoader : public ChainClient { public: - //! Create new wallet. - virtual std::unique_ptr createWallet(const std::string& name, const SecureString& passphrase, uint64_t wallet_creation_flags, bilingual_str& error, std::vector& warnings) = 0; + //! Register non-core wallet RPCs + virtual void registerOtherRpcs(const Span& commands) = 0; + + //! Create new wallet. + virtual std::unique_ptr createWallet(const std::string& name, const SecureString& passphrase, uint64_t wallet_creation_flags, bilingual_str& error, std::vector& warnings) = 0; //! Load existing wallet. virtual std::unique_ptr loadWallet(const std::string& name, bilingual_str& error, std::vector& warnings) = 0; @@ -454,7 +461,8 @@ std::unique_ptr MakeWallet(const std::shared_ptr& wallet); //! Return implementation of ChainClient interface for a wallet loader. This //! function will be undefined in builds where ENABLE_WALLET is false. -std::unique_ptr MakeWalletLoader(Chain& chain, ArgsManager& args, const std::unique_ptr& coinjoin_loader); +std::unique_ptr MakeWalletLoader(Chain& chain, ArgsManager& args, NodeContext& node_context, + const std::unique_ptr& coinjoin_loader); } // namespace interfaces diff --git a/src/rpc/coinjoin.cpp b/src/rpc/coinjoin.cpp index 81f6c3793346b..f091371b7f5e3 100644 --- a/src/rpc/coinjoin.cpp +++ b/src/rpc/coinjoin.cpp @@ -2,16 +2,17 @@ // Distributed under the MIT/X11 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 #ifdef ENABLE_WALLET #include @@ -464,14 +465,13 @@ static RPCHelpMan getcoinjoininfo() }; } -void RegisterCoinJoinRPCCommands(CRPCTable &t) +#ifdef ENABLE_WALLET +Span GetWalletCoinJoinRPCCommands() { // clang-format off static const CRPCCommand commands[] = -{ // category actor (function) - // --------------------- ----------------------- - { "dash", &getcoinjoininfo, }, -#ifdef ENABLE_WALLET +{ // category actor (function) + // --------------------- ----------------------- { "dash", &coinjoin, }, { "dash", &coinjoin_reset, }, { "dash", &coinjoin_start, }, @@ -480,12 +480,37 @@ static const CRPCCommand commands[] = { "dash", &coinjoinsalt_generate, }, { "dash", &coinjoinsalt_get, }, { "dash", &coinjoinsalt_set, }, + { "dash", &getcoinjoininfo, }, { "hidden", &getpoolinfo, }, +}; +// clang-format on + return commands; +} #endif // ENABLE_WALLET + +void RegisterCoinJoinRPCCommands(CRPCTable& t) +{ +// clang-format off +static const CRPCCommand commands_wallet[] = +{ // category actor (function) + // --------------------- ----------------------- + { "dash", &getcoinjoininfo, }, }; // clang-format on - for (const auto& command : commands) { - t.appendCommand(command.name, &command); + // If we aren't compiling with wallet support, we still need to register RPCs that are + // capable of working without wallet support. We have to do this even if wallet support + // is compiled in but is disabled at runtime because runtime disablement prohibits + // registering wallet RPCs. We still want the reduced functionality RPC to be registered. + // TODO: Spin off these hybrid RPCs into dedicated wallet-only and/or wallet-free RPCs + // and get rid of this workaround. + if (!g_wallet_init_interface.HasWalletSupport() +#ifdef ENABLE_WALLET + || gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET) +#endif // ENABLE_WALLET + ) { + for (const auto& command : commands_wallet) { + tableRPC.appendCommand(command.name, &command); + } } } diff --git a/src/rpc/evo.cpp b/src/rpc/evo.cpp index cd18a04976c55..c3a9ccbb70bfe 100644 --- a/src/rpc/evo.cpp +++ b/src/rpc/evo.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #ifdef ENABLE_WALLET #include @@ -1778,17 +1779,15 @@ static RPCHelpMan bls_help() }; } -void RegisterEvoRPCCommands(CRPCTable &tableRPC) +#ifdef ENABLE_WALLET +Span GetWalletEvoRPCCommands() { // clang-format off static const CRPCCommand commands[] = { // category actor (function) // --------------------- ----------------------- - { "evo", &bls_help, }, - { "evo", &bls_generate, }, - { "evo", &bls_fromsecret, }, - { "evo", &protx_help, }, -#ifdef ENABLE_WALLET + { "evo", &protx_list, }, + { "evo", &protx_info, }, { "evo", &protx_register, }, { "evo", &protx_register_evo, }, { "evo", &protx_register_legacy, }, @@ -1804,14 +1803,47 @@ static const CRPCCommand commands[] = { "evo", &protx_update_registrar, }, { "evo", &protx_update_registrar_legacy, }, { "evo", &protx_revoke, }, -#endif - { "evo", &protx_list, }, - { "evo", &protx_info, }, +}; +// clang-format on + return commands; +} +#endif // ENABLE_WALLET + +void RegisterEvoRPCCommands(CRPCTable& tableRPC) +{ +// clang-format off +static const CRPCCommand commands[] = +{ // category actor (function) + // --------------------- ----------------------- + { "evo", &bls_help, }, + { "evo", &bls_generate, }, + { "evo", &bls_fromsecret, }, + { "evo", &protx_help, }, { "evo", &protx_diff, }, { "evo", &protx_listdiff, }, }; +static const CRPCCommand commands_wallet[] = +{ + { "evo", &protx_list, }, + { "evo", &protx_info, }, +}; // clang-format on for (const auto& command : commands) { tableRPC.appendCommand(command.name, &command); } + // If we aren't compiling with wallet support, we still need to register RPCs that are + // capable of working without wallet support. We have to do this even if wallet support + // is compiled in but is disabled at runtime because runtime disablement prohibits + // registering wallet RPCs. We still want the reduced functionality RPC to be registered. + // TODO: Spin off these hybrid RPCs into dedicated wallet-only and/or wallet-free RPCs + // and get rid of this workaround. + if (!g_wallet_init_interface.HasWalletSupport() +#ifdef ENABLE_WALLET + || gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET) +#endif // ENABLE_WALLET + ) { + for (const auto& command : commands_wallet) { + tableRPC.appendCommand(command.name, &command); + } + } } diff --git a/src/rpc/governance.cpp b/src/rpc/governance.cpp index 2671149417f52..4a728b852dc51 100644 --- a/src/rpc/governance.cpp +++ b/src/rpc/governance.cpp @@ -1072,6 +1072,23 @@ static RPCHelpMan getsuperblockbudget() }; } +#ifdef ENABLE_WALLET +Span GetWalletGovernanceRPCCommands() +{ +// clang-format off +static const CRPCCommand commands[] = +{ // category actor (function) + // --------------------- ----------------------- + { "dash", &gobject_prepare, }, + { "dash", &gobject_list_prepared, }, + { "dash", &gobject_vote_many, }, + { "dash", &gobject_vote_alias, }, +}; +// clang-format on + return commands; +} +#endif // ENABLE_WALLET + void RegisterGovernanceRPCCommands(CRPCTable &t) { // clang-format off @@ -1085,12 +1102,6 @@ static const CRPCCommand commands[] = { "dash", &gobject_count, }, { "dash", &gobject_deserialize, }, { "dash", &gobject_check, }, -#ifdef ENABLE_WALLET - { "dash", &gobject_prepare, }, - { "dash", &gobject_list_prepared, }, - { "dash", &gobject_vote_many, }, - { "dash", &gobject_vote_alias, }, -#endif { "dash", &gobject_submit, }, { "dash", &gobject_list, }, { "dash", &gobject_diff, }, diff --git a/src/rpc/masternode.cpp b/src/rpc/masternode.cpp index 4731ce204dec4..166a5ce3c3e4b 100644 --- a/src/rpc/masternode.cpp +++ b/src/rpc/masternode.cpp @@ -735,6 +735,20 @@ static RPCHelpMan masternodelist_composite() return masternodelist_helper(true); } +#ifdef ENABLE_WALLET +Span GetWalletMasternodeRPCCommands() +{ +// clang-format off +static const CRPCCommand commands[] = +{ // category actor (function) + // --------------------- ----------------------- + { "dash", &masternode_outputs, }, +}; +// clang-format on + return commands; +} +#endif // ENABLE_WALLET + void RegisterMasternodeRPCCommands(CRPCTable &t) { // clang-format off @@ -746,9 +760,6 @@ static const CRPCCommand commands[] = { "dash", &masternodelist, }, { "dash", &masternode_connect, }, { "dash", &masternode_count, }, -#ifdef ENABLE_WALLET - { "dash", &masternode_outputs, }, -#endif // ENABLE_WALLET { "dash", &masternode_status, }, { "dash", &masternode_payments, }, { "dash", &masternode_winners, }, diff --git a/src/rpc/register.h b/src/rpc/register.h index d1360e2bd0748..114de4477424d 100644 --- a/src/rpc/register.h +++ b/src/rpc/register.h @@ -7,8 +7,12 @@ /** These are in one header file to avoid creating tons of single-function * headers for everything under src/rpc/ */ +class CRPCCommand; class CRPCTable; +template +class Span; + void RegisterBlockchainRPCCommands(CRPCTable &tableRPC); void RegisterFeeRPCCommands(CRPCTable&); void RegisterMempoolRPCCommands(CRPCTable&); @@ -24,6 +28,14 @@ void RegisterGovernanceRPCCommands(CRPCTable &tableRPC); void RegisterEvoRPCCommands(CRPCTable &tableRPC); void RegisterQuorumsRPCCommands(CRPCTable &tableRPC); +#ifdef ENABLE_WALLET +// Dash-specific wallet-only RPC commands +Span GetWalletCoinJoinRPCCommands(); +Span GetWalletEvoRPCCommands(); +Span GetWalletGovernanceRPCCommands(); +Span GetWalletMasternodeRPCCommands(); +#endif // ENABLE_WALLET + static inline void RegisterAllCoreRPCCommands(CRPCTable &t) { RegisterBlockchainRPCCommands(t); diff --git a/src/rpc/server_util.cpp b/src/rpc/server_util.cpp index d533934075d03..2700a7851f8ea 100644 --- a/src/rpc/server_util.cpp +++ b/src/rpc/server_util.cpp @@ -13,15 +13,29 @@ #include #include +#ifdef ENABLE_WALLET +#include +#endif // ENABLE_WALLET + #include NodeContext& EnsureAnyNodeContext(const CoreContext& context) { auto* const node_context = GetContext(context); - if (!node_context) { - throw JSONRPCError(RPC_INTERNAL_ERROR, "Node context not found"); + if (node_context) { + return *node_context; + } +#ifdef ENABLE_WALLET + // We're now going to try our luck with WalletContext on the off chance + // we're being called by a wallet RPC that's trying to access NodeContext + // See comment on WalletContext::node_context for more information. + // TODO: Find a solution that removes the need for this workaround + auto* const wallet_context = GetContext(context); + if (wallet_context && wallet_context->node_context) { + return *wallet_context->node_context; } - return *node_context; +#endif // ENABLE_WALLET + throw JSONRPCError(RPC_INTERNAL_ERROR, "Node context not found"); } CTxMemPool& EnsureMemPool(const NodeContext& node) diff --git a/src/wallet/context.h b/src/wallet/context.h index cda736798035f..cab65ec0c7e4b 100644 --- a/src/wallet/context.h +++ b/src/wallet/context.h @@ -8,6 +8,7 @@ #include class ArgsManager; +struct NodeContext; namespace interfaces { class Chain; namespace CoinJoin { @@ -31,6 +32,12 @@ struct WalletContext { // TODO: replace this unique_ptr to a pointer // probably possible to do after bitcoin/bitcoin#22219 const std::unique_ptr& coinjoin_loader; + // Some Dash RPCs rely on WalletContext yet access NodeContext members + // even though wallet RPCs should refrain from accessing non-wallet + // capabilities (even though it is a hard ask sometimes). We should get + // rid of this at some point but until then, here's NodeContext. + // TODO: Get rid of this. It's not nice. + NodeContext* node_context{nullptr}; //! Declare default constructor and destructor that are not inline, so code //! instantiating the WalletContext struct doesn't need to #include class diff --git a/src/wallet/init.cpp b/src/wallet/init.cpp index 206c628db81bc..66a8c8a33f73e 100644 --- a/src/wallet/init.cpp +++ b/src/wallet/init.cpp @@ -187,7 +187,7 @@ void WalletInit::Construct(NodeContext& node) const LogPrintf("Wallet disabled!\n"); return; } - auto wallet_loader = interfaces::MakeWalletLoader(*node.chain, args, node.coinjoin_loader); + auto wallet_loader = interfaces::MakeWalletLoader(*node.chain, args, node, node.coinjoin_loader); node.wallet_loader = wallet_loader.get(); node.chain_clients.emplace_back(std::move(wallet_loader)); } diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index aa1930838f51f..8768bbf3daa43 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -564,26 +564,34 @@ class WalletImpl : public Wallet class WalletLoaderImpl : public WalletLoader { +private: + void RegisterRPCs(const Span& commands) + { + for (const CRPCCommand& command : commands) { + m_rpc_commands.emplace_back(command.category, command.name, [this, &command](const JSONRPCRequest& request, UniValue& result, bool last_handler) { + JSONRPCRequest wallet_request = request; + wallet_request.context = m_context; + return command.actor(wallet_request, result, last_handler); + }, command.argNames, command.unique_id); + m_rpc_handlers.emplace_back(m_context.chain->handleRpc(m_rpc_commands.back())); + } + } + public: - WalletLoaderImpl(Chain& chain, ArgsManager& args, const std::unique_ptr& coinjoin_loader) + WalletLoaderImpl(Chain& chain, ArgsManager& args, NodeContext& node_context, + const std::unique_ptr& coinjoin_loader) : m_context{coinjoin_loader} { - m_context.chain = &chain; m_context.args = &args; + m_context.chain = &chain; + m_context.node_context = &node_context; } ~WalletLoaderImpl() override { UnloadWallets(); } //! ChainClient methods void registerRpcs() override { - for (const CRPCCommand& command : GetWalletRPCCommands()) { - m_rpc_commands.emplace_back(command.category, command.name, [this, &command](const JSONRPCRequest& request, UniValue& result, bool last_handler) { - JSONRPCRequest wallet_request = request; - wallet_request.context = m_context; - return command.actor(wallet_request, result, last_handler); - }, command.argNames, command.unique_id); - m_rpc_handlers.emplace_back(m_context.chain->handleRpc(m_rpc_commands.back())); - } + RegisterRPCs(GetWalletRPCCommands()); } bool verify() override { return VerifyWallets(m_context); } bool load() override { assert(m_context.coinjoin_loader); return LoadWallets(m_context); } @@ -593,6 +601,10 @@ class WalletLoaderImpl : public WalletLoader void setMockTime(int64_t time) override { return SetMockTime(time); } //! WalletLoader methods + void registerOtherRpcs(const Span& commands) override + { + return RegisterRPCs(commands); + } std::unique_ptr createWallet(const std::string& name, const SecureString& passphrase, uint64_t wallet_creation_flags, bilingual_str& error, std::vector& warnings) override { std::shared_ptr wallet; @@ -654,7 +666,8 @@ class WalletLoaderImpl : public WalletLoader namespace interfaces { std::unique_ptr MakeWallet(const std::shared_ptr& wallet) { return wallet ? std::make_unique(wallet) : nullptr; } -std::unique_ptr MakeWalletLoader(Chain& chain, ArgsManager& args, const std::unique_ptr& coinjoin_loader) { - return std::make_unique(chain, args, coinjoin_loader); +std::unique_ptr MakeWalletLoader(Chain& chain, ArgsManager& args, NodeContext& node_context, + const std::unique_ptr& coinjoin_loader) { + return std::make_unique(chain, args, node_context, coinjoin_loader); } } // namespace interfaces diff --git a/src/wallet/test/init_test_fixture.cpp b/src/wallet/test/init_test_fixture.cpp index 88df028e6fa8a..a8dd8b67b4d5a 100644 --- a/src/wallet/test/init_test_fixture.cpp +++ b/src/wallet/test/init_test_fixture.cpp @@ -14,7 +14,7 @@ InitWalletDirTestingSetup::InitWalletDirTestingSetup(const std::string& chainName) : BasicTestingSetup(chainName) { - m_wallet_loader = MakeWalletLoader(*m_node.chain, *Assert(m_node.args), m_node.coinjoin_loader); + m_wallet_loader = MakeWalletLoader(*m_node.chain, *Assert(m_node.args), m_node, m_node.coinjoin_loader); std::string sep; sep += fs::path::preferred_separator; diff --git a/src/wallet/test/wallet_test_fixture.cpp b/src/wallet/test/wallet_test_fixture.cpp index a72cd7b667bca..eddb54690d392 100644 --- a/src/wallet/test/wallet_test_fixture.cpp +++ b/src/wallet/test/wallet_test_fixture.cpp @@ -8,7 +8,7 @@ WalletTestingSetup::WalletTestingSetup(const std::string& chainName) : TestingSetup(chainName), - m_wallet_loader{interfaces::MakeWalletLoader(*m_node.chain, *Assert(m_node.args), m_node.coinjoin_loader)}, + m_wallet_loader{interfaces::MakeWalletLoader(*m_node.chain, *Assert(m_node.args), m_node, m_node.coinjoin_loader)}, m_wallet(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateMockWalletDatabase()) { m_wallet.LoadWallet(); From c4ea5eef1fefbf9c94ff50243e4c53f10f763f71 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Thu, 9 Jan 2025 20:45:47 +0000 Subject: [PATCH 11/16] merge bitcoin#19101: remove ::vpwallets and related global variables This is the second half of the backport, which deals with deglobalizing wallet-specific variables like vpwallets, cs_wallets and friends. --- src/interfaces/wallet.h | 2 +- src/qt/test/addressbooktests.cpp | 7 ++-- src/qt/test/wallettests.cpp | 7 ++-- src/wallet/context.h | 12 ++++++ src/wallet/interfaces.cpp | 25 ++++++------ src/wallet/load.cpp | 22 +++++------ src/wallet/load.h | 6 +-- src/wallet/rpcwallet.cpp | 13 ++++--- src/wallet/test/coinjoin_tests.cpp | 8 +++- src/wallet/test/wallet_tests.cpp | 51 ++++++++++++++---------- src/wallet/wallet.cpp | 62 ++++++++++++++---------------- src/wallet/wallet.h | 14 +++---- src/wallet/walletdb.cpp | 4 +- src/wallet/walletdb.h | 3 +- 14 files changed, 132 insertions(+), 104 deletions(-) diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index b0bb51c7cd298..481b27f562a5f 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -457,7 +457,7 @@ struct WalletTxOut //! Return implementation of Wallet interface. This function is defined in //! dummywallet.cpp and throws if the wallet component is not compiled. -std::unique_ptr MakeWallet(const std::shared_ptr& wallet); +std::unique_ptr MakeWallet(WalletContext& context, const std::shared_ptr& wallet); //! Return implementation of ChainClient interface for a wallet loader. This //! function will be undefined in builds where ENABLE_WALLET is false. diff --git a/src/qt/test/addressbooktests.cpp b/src/qt/test/addressbooktests.cpp index 3e0be6da11a69..e9f4ff46fbac4 100644 --- a/src/qt/test/addressbooktests.cpp +++ b/src/qt/test/addressbooktests.cpp @@ -108,9 +108,10 @@ void TestAddAddressesToSendBook(interfaces::Node& node) // Initialize relevant QT models. OptionsModel optionsModel; ClientModel clientModel(node, &optionsModel); - AddWallet(wallet); - WalletModel walletModel(interfaces::MakeWallet(wallet), clientModel); - RemoveWallet(wallet, std::nullopt); + WalletContext& context = *node.walletLoader().context(); + AddWallet(context, wallet); + WalletModel walletModel(interfaces::MakeWallet(context, wallet), clientModel); + RemoveWallet(context, wallet, /*load_on_startup=*/std::nullopt); EditAddressDialog editAddressDialog(EditAddressDialog::NewSendingAddress); editAddressDialog.setModel(walletModel.getAddressTableModel()); diff --git a/src/qt/test/wallettests.cpp b/src/qt/test/wallettests.cpp index fe16db6c4e7e7..e71293ea9a84e 100644 --- a/src/qt/test/wallettests.cpp +++ b/src/qt/test/wallettests.cpp @@ -109,8 +109,9 @@ void TestGUI(interfaces::Node& node) test.CreateAndProcessBlock({}, GetScriptForRawPubKey(test.coinbaseKey.GetPubKey())); } node.setContext(&test.m_node); + WalletContext& context = *node.walletLoader().context(); std::shared_ptr wallet = std::make_shared(node.context()->chain.get(), node.context()->coinjoin_loader.get(), "", CreateMockWalletDatabase()); - AddWallet(wallet); + AddWallet(context, wallet); wallet->LoadWallet(); { auto spk_man = wallet->GetOrCreateLegacyScriptPubKeyMan(); @@ -134,7 +135,7 @@ void TestGUI(interfaces::Node& node) TransactionView transactionView; OptionsModel optionsModel; ClientModel clientModel(node, &optionsModel); - WalletModel walletModel(interfaces::MakeWallet(wallet), clientModel); + WalletModel walletModel(interfaces::MakeWallet(context, wallet), clientModel); sendCoinsDialog.setModel(&walletModel); transactionView.setModel(&walletModel); @@ -246,7 +247,7 @@ void TestGUI(interfaces::Node& node) QPushButton* removeRequestButton = receiveCoinsDialog.findChild("removeRequestButton"); removeRequestButton->click(); QCOMPARE(requestTableModel->rowCount({}), currentRowCount-1); - RemoveWallet(wallet, std::nullopt); + RemoveWallet(context, wallet, /*load_on_startup=*/std::nullopt); // Check removal from wallet QCOMPARE(walletModel.wallet().getAddressReceiveRequests().size(), size_t{0}); diff --git a/src/wallet/context.h b/src/wallet/context.h index cab65ec0c7e4b..bdda7b26ebe9e 100644 --- a/src/wallet/context.h +++ b/src/wallet/context.h @@ -5,17 +5,26 @@ #ifndef BITCOIN_WALLET_CONTEXT_H #define BITCOIN_WALLET_CONTEXT_H +#include + +#include +#include #include +#include class ArgsManager; +class CWallet; struct NodeContext; namespace interfaces { class Chain; namespace CoinJoin { class Loader; } // namspace CoinJoin +class Wallet; } // namespace interfaces +using LoadWalletFn = std::function wallet)>; + //! WalletContext struct containing references to state shared between CWallet //! instances, like the reference to the chain interface, and the list of opened //! wallets. @@ -29,6 +38,9 @@ class Loader; struct WalletContext { interfaces::Chain* chain{nullptr}; ArgsManager* args{nullptr}; // Currently a raw pointer because the memory is not managed by this struct + Mutex wallets_mutex; + std::vector> wallets GUARDED_BY(wallets_mutex); + std::list wallet_load_fns GUARDED_BY(wallets_mutex); // TODO: replace this unique_ptr to a pointer // probably possible to do after bitcoin/bitcoin#22219 const std::unique_ptr& coinjoin_loader; diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index 8768bbf3daa43..cdd021d8f9a69 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -127,7 +127,7 @@ WalletTxOut MakeWalletTxOut(const CWallet& wallet, class WalletImpl : public Wallet { public: - explicit WalletImpl(const std::shared_ptr& wallet) : m_wallet(wallet) {} + explicit WalletImpl(WalletContext& context, const std::shared_ptr& wallet) : m_context(context), m_wallet(wallet) {} void markDirty() override { @@ -512,7 +512,7 @@ class WalletImpl : public Wallet CAmount getDefaultMaxTxFee() override { return m_wallet->m_default_max_tx_fee; } void remove() override { - RemoveWallet(m_wallet, false /* load_on_start */); + RemoveWallet(m_context, m_wallet, false /* load_on_start */); } bool isLegacy() override { return m_wallet->IsLegacy(); } std::unique_ptr handleUnload(UnloadFn fn) override @@ -558,6 +558,7 @@ class WalletImpl : public Wallet } CWallet* wallet() override { return m_wallet.get(); } + WalletContext& m_context; std::shared_ptr m_wallet; std::unique_ptr m_coinjoin_client; }; @@ -586,7 +587,7 @@ class WalletLoaderImpl : public WalletLoader m_context.chain = &chain; m_context.node_context = &node_context; } - ~WalletLoaderImpl() override { UnloadWallets(); } + ~WalletLoaderImpl() override { UnloadWallets(m_context); } //! ChainClient methods void registerRpcs() override @@ -596,8 +597,8 @@ class WalletLoaderImpl : public WalletLoader bool verify() override { return VerifyWallets(m_context); } bool load() override { assert(m_context.coinjoin_loader); return LoadWallets(m_context); } void start(CScheduler& scheduler) override { return StartWallets(m_context, scheduler); } - void flush() override { return FlushWallets(); } - void stop() override { return StopWallets(); } + void flush() override { return FlushWallets(m_context); } + void stop() override { return StopWallets(m_context); } void setMockTime(int64_t time) override { return SetMockTime(time); } //! WalletLoader methods @@ -614,7 +615,7 @@ class WalletLoaderImpl : public WalletLoader options.create_flags = wallet_creation_flags; options.create_passphrase = passphrase; assert(m_context.coinjoin_loader); - return MakeWallet(CreateWallet(m_context, name, true /* load_on_start */, options, status, error, warnings)); + return MakeWallet(m_context, CreateWallet(m_context, name, true /* load_on_start */, options, status, error, warnings)); } std::unique_ptr loadWallet(const std::string& name, bilingual_str& error, std::vector& warnings) override { @@ -622,13 +623,13 @@ class WalletLoaderImpl : public WalletLoader DatabaseStatus status; options.require_existing = true; assert(m_context.coinjoin_loader); - return MakeWallet(LoadWallet(m_context, name, true /* load_on_start */, options, status, error, warnings)); + return MakeWallet(m_context, LoadWallet(m_context, name, true /* load_on_start */, options, status, error, warnings)); } std::unique_ptr restoreWallet(const fs::path& backup_file, const std::string& wallet_name, bilingual_str& error, std::vector& warnings) override { DatabaseStatus status; assert(m_context.coinjoin_loader); - return MakeWallet(RestoreWallet(m_context, backup_file, wallet_name, /*load_on_start=*/true, status, error, warnings)); + return MakeWallet(m_context, RestoreWallet(m_context, backup_file, wallet_name, /*load_on_start=*/true, status, error, warnings)); } std::string getWalletDir() override { @@ -645,14 +646,14 @@ class WalletLoaderImpl : public WalletLoader std::vector> getWallets() override { std::vector> wallets; - for (const auto& wallet : GetWallets()) { - wallets.emplace_back(MakeWallet(wallet)); + for (const auto& wallet : GetWallets(m_context)) { + wallets.emplace_back(MakeWallet(m_context, wallet)); } return wallets; } std::unique_ptr handleLoadWallet(LoadWalletFn fn) override { - return HandleLoadWallet(std::move(fn)); + return HandleLoadWallet(m_context, std::move(fn)); } WalletContext* context() override { return &m_context; } @@ -665,7 +666,7 @@ class WalletLoaderImpl : public WalletLoader } // namespace wallet namespace interfaces { -std::unique_ptr MakeWallet(const std::shared_ptr& wallet) { return wallet ? std::make_unique(wallet) : nullptr; } +std::unique_ptr MakeWallet(WalletContext& context, const std::shared_ptr& wallet) { return wallet ? std::make_unique(context, wallet) : nullptr; } std::unique_ptr MakeWalletLoader(Chain& chain, ArgsManager& args, NodeContext& node_context, const std::unique_ptr& coinjoin_loader) { return std::make_unique(chain, args, node_context, coinjoin_loader); diff --git a/src/wallet/load.cpp b/src/wallet/load.cpp index 8696396c159de..c06e8184ddf33 100644 --- a/src/wallet/load.cpp +++ b/src/wallet/load.cpp @@ -125,7 +125,7 @@ bool LoadWallets(WalletContext& context) chain.initError(error_string); return false; } - AddWallet(pwallet); + AddWallet(context, pwallet); } return true; } catch (const std::runtime_error& e) { @@ -136,20 +136,20 @@ bool LoadWallets(WalletContext& context) void StartWallets(WalletContext& context, CScheduler& scheduler) { - for (const std::shared_ptr& pwallet : GetWallets()) { + for (const std::shared_ptr& pwallet : GetWallets(context)) { pwallet->postInitProcess(); } // Schedule periodic wallet flushes and tx rebroadcasts if (context.args->GetBoolArg("-flushwallet", DEFAULT_FLUSHWALLET)) { - scheduler.scheduleEvery(MaybeCompactWalletDB, std::chrono::milliseconds{500}); + scheduler.scheduleEvery([&context] { MaybeCompactWalletDB(context); }, std::chrono::milliseconds{500}); } - scheduler.scheduleEvery(MaybeResendWalletTxs, std::chrono::milliseconds{1000}); + scheduler.scheduleEvery([&context] { MaybeResendWalletTxs(context); }, std::chrono::milliseconds{1000}); } -void FlushWallets() +void FlushWallets(WalletContext& context) { - for (const std::shared_ptr& pwallet : GetWallets()) { + for (const std::shared_ptr& pwallet : GetWallets(context)) { if (CCoinJoinClientOptions::IsEnabled()) { // Stop CoinJoin, release keys pwallet->coinjoin_loader().FlushWallet(pwallet->GetName()); @@ -158,21 +158,21 @@ void FlushWallets() } } -void StopWallets() +void StopWallets(WalletContext& context) { - for (const std::shared_ptr& pwallet : GetWallets()) { + for (const std::shared_ptr& pwallet : GetWallets(context)) { pwallet->Close(); } } -void UnloadWallets() +void UnloadWallets(WalletContext& context) { - auto wallets = GetWallets(); + auto wallets = GetWallets(context); while (!wallets.empty()) { auto wallet = wallets.back(); wallets.pop_back(); std::vector warnings; - RemoveWallet(wallet, std::nullopt, warnings); + RemoveWallet(context, wallet, /*load_on_startup=*/std::nullopt, warnings); UnloadWallet(std::move(wallet)); } } diff --git a/src/wallet/load.h b/src/wallet/load.h index 0c02d8d71ace5..5893a30aa628f 100644 --- a/src/wallet/load.h +++ b/src/wallet/load.h @@ -31,12 +31,12 @@ bool LoadWallets(WalletContext& context); void StartWallets(WalletContext& context, CScheduler& scheduler); //! Flush all wallets in preparation for shutdown. -void FlushWallets(); +void FlushWallets(WalletContext& context); //! Stop all wallets. Wallets will be flushed first. -void StopWallets(); +void StopWallets(WalletContext& context); //! Close all wallets. -void UnloadWallets(); +void UnloadWallets(WalletContext& context); #endif // BITCOIN_WALLET_LOAD_H diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index e5c2cc76156d7..7ec58273347e8 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -100,15 +100,16 @@ bool GetWalletNameFromJSONRPCRequest(const JSONRPCRequest& request, std::string& std::shared_ptr GetWalletForJSONRPCRequest(const JSONRPCRequest& request) { CHECK_NONFATAL(request.mode == JSONRPCRequest::EXECUTE); + WalletContext& context = EnsureWalletContext(request.context); std::string wallet_name; if (GetWalletNameFromJSONRPCRequest(request, wallet_name)) { - std::shared_ptr pwallet = GetWallet(wallet_name); + std::shared_ptr pwallet = GetWallet(context, wallet_name); if (!pwallet) throw JSONRPCError(RPC_WALLET_NOT_FOUND, "Requested wallet does not exist or is not loaded"); return pwallet; } - std::vector> wallets = GetWallets(); + std::vector> wallets = GetWallets(context); if (wallets.size() == 1) { return wallets[0]; } @@ -2741,7 +2742,8 @@ static RPCHelpMan listwallets() { UniValue obj(UniValue::VARR); - for (const std::shared_ptr& wallet : GetWallets()) { + WalletContext& context = EnsureWalletContext(request.context); + for (const std::shared_ptr& wallet : GetWallets(context)) { LOCK(wallet->cs_wallet); obj.push_back(wallet->GetName()); } @@ -3160,7 +3162,8 @@ static RPCHelpMan unloadwallet() wallet_name = request.params[0].get_str(); } - std::shared_ptr wallet = GetWallet(wallet_name); + WalletContext& context = EnsureWalletContext(request.context); + std::shared_ptr wallet = GetWallet(context, wallet_name); if (!wallet) { throw JSONRPCError(RPC_WALLET_NOT_FOUND, "Requested wallet does not exist or is not loaded"); } @@ -3170,7 +3173,7 @@ static RPCHelpMan unloadwallet() // is destroyed (see CheckUniqueFileid). std::vector warnings; std::optional load_on_start = request.params[1].isNull() ? std::nullopt : std::optional(request.params[1].get_bool()); - if (!RemoveWallet(wallet, load_on_start, warnings)) { + if (!RemoveWallet(context, wallet, load_on_start, warnings)) { throw JSONRPCError(RPC_MISC_ERROR, "Requested wallet already unloaded"); } diff --git a/src/wallet/test/coinjoin_tests.cpp b/src/wallet/test/coinjoin_tests.cpp index 9335ae0098e9b..d59549e4c85ed 100644 --- a/src/wallet/test/coinjoin_tests.cpp +++ b/src/wallet/test/coinjoin_tests.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -129,12 +130,14 @@ class CTransactionBuilderTestSetup : public TestChain100Setup { public: CTransactionBuilderTestSetup() + : context{m_node.coinjoin_loader} { + context.chain = m_node.chain.get(); CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); wallet = std::make_unique(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateMockWalletDatabase()); wallet->SetupLegacyScriptPubKeyMan(); wallet->LoadWallet(); - AddWallet(wallet); + AddWallet(context, wallet); { LOCK2(wallet->cs_wallet, cs_main); wallet->GetLegacyScriptPubKeyMan()->AddKeyPubKey(coinbaseKey, coinbaseKey.GetPubKey()); @@ -148,9 +151,10 @@ class CTransactionBuilderTestSetup : public TestChain100Setup ~CTransactionBuilderTestSetup() { - RemoveWallet(wallet, std::nullopt); + RemoveWallet(context, wallet, /*load_on_startup=*/std::nullopt); } + WalletContext context; std::shared_ptr wallet; CWalletTx& AddTxToChain(uint256 nTxHash) diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index 9f693dc6994c4..3998758b6fb7f 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -58,7 +58,7 @@ static std::shared_ptr TestLoadWallet(WalletContext& context) auto wallet = CWallet::Create(context, "", std::move(database), options.create_flags, error, warnings); if (context.coinjoin_loader) { // TODO: see CreateWalletWithoutChain - AddWallet(wallet); + AddWallet(context, wallet); } if (context.chain) { wallet->postInitProcess(); @@ -66,12 +66,12 @@ static std::shared_ptr TestLoadWallet(WalletContext& context) return wallet; } -static void TestUnloadWallet(std::shared_ptr&& wallet) +static void TestUnloadWallet(WalletContext& context, std::shared_ptr&& wallet) { std::vector warnings; SyncWithValidationInterfaceQueue(); wallet->m_chain_notifications_handler.reset(); - RemoveWallet(wallet, std::nullopt, warnings); + RemoveWallet(context, wallet, /*load_on_startup=*/std::nullopt, warnings); UnloadWallet(std::move(wallet)); } @@ -221,7 +221,8 @@ BOOST_FIXTURE_TEST_CASE(importmulti_rescan, TestChain100Setup) std::shared_ptr wallet = std::make_shared(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateDummyWalletDatabase()); wallet->SetupLegacyScriptPubKeyMan(); WITH_LOCK(wallet->cs_wallet, wallet->SetLastBlockProcessed(newTip->nHeight, newTip->GetBlockHash())); - AddWallet(wallet); + WalletContext context{m_node.coinjoin_loader}; + AddWallet(context, wallet); UniValue keys; keys.setArray(); UniValue key; @@ -239,6 +240,7 @@ BOOST_FIXTURE_TEST_CASE(importmulti_rescan, TestChain100Setup) key.pushKV("internal", UniValue(true)); keys.push_back(key); JSONRPCRequest request; + request.context = context; request.params.setArray(); request.params.push_back(keys); @@ -252,7 +254,7 @@ BOOST_FIXTURE_TEST_CASE(importmulti_rescan, TestChain100Setup) "downloading and rescanning the relevant blocks (see -reindex and -rescan " "options).\"}},{\"success\":true}]", 0, oldTip->GetBlockTimeMax(), TIMESTAMP_WINDOW)); - RemoveWallet(wallet, std::nullopt); + RemoveWallet(context, wallet, /*load_on_startup=*/std::nullopt); } } @@ -279,6 +281,7 @@ BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup) // Import key into wallet and call dumpwallet to create backup file. { + WalletContext context{m_node.coinjoin_loader}; std::shared_ptr wallet = std::make_shared(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateDummyWalletDatabase()); { auto spk_man = wallet->GetOrCreateLegacyScriptPubKeyMan(); @@ -286,15 +289,16 @@ BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup) spk_man->mapKeyMetadata[coinbaseKey.GetPubKey().GetID()].nCreateTime = KEY_TIME; spk_man->AddKeyPubKey(coinbaseKey, coinbaseKey.GetPubKey()); - AddWallet(wallet); + AddWallet(context, wallet); wallet->SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); } JSONRPCRequest request; + request.context = context; request.params.setArray(); request.params.push_back(backup_file); ::dumpwallet().HandleRequest(request); - RemoveWallet(wallet, std::nullopt); + RemoveWallet(context, wallet, /*load_on_startup=*/std::nullopt); } // Call importwallet RPC and verify all blocks with timestamps >= BLOCK_TIME @@ -304,13 +308,15 @@ BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup) LOCK(wallet->cs_wallet); wallet->SetupLegacyScriptPubKeyMan(); + WalletContext context{m_node.coinjoin_loader}; JSONRPCRequest request; + request.context = context; request.params.setArray(); request.params.push_back(backup_file); - AddWallet(wallet); + AddWallet(context, wallet); wallet->SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); ::importwallet().HandleRequest(request); - RemoveWallet(wallet, std::nullopt); + RemoveWallet(context, wallet, /*load_on_startup=*/std::nullopt); BOOST_CHECK_EQUAL(wallet->mapWallet.size(), 3U); BOOST_CHECK_EQUAL(m_coinbase_txns.size(), 103U); @@ -705,7 +711,7 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup) CKey key; key.MakeNewKey(true); AddKey(*wallet, key); - TestUnloadWallet(std::move(wallet)); + TestUnloadWallet(context, std::move(wallet)); // Add log hook to detect AddToWallet events from rescans, blockConnected, @@ -757,7 +763,7 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup) SyncWithValidationInterfaceQueue(); BOOST_CHECK_EQUAL(addtx_count, 4); - TestUnloadWallet(std::move(wallet)); + TestUnloadWallet(context, std::move(wallet)); // Load wallet again, this time creating new block and mempool transactions @@ -767,7 +773,7 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup) // deadlock during the sync and simulates a new block notification happening // as soon as possible. addtx_count = 0; - auto handler = HandleLoadWallet([&](std::unique_ptr wallet) { + auto handler = HandleLoadWallet(context, [&](std::unique_ptr wallet) { BOOST_CHECK(rescan_completed); m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); block_tx = TestSimpleSpend(*m_coinbase_txns[2], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); @@ -784,7 +790,7 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup) BOOST_CHECK_EQUAL(wallet->mapWallet.count(mempool_tx.GetHash()), 1U); } - TestUnloadWallet(std::move(wallet)); + TestUnloadWallet(context, std::move(wallet)); } BOOST_FIXTURE_TEST_CASE(CreateWalletWithoutChain, BasicTestingSetup) @@ -827,7 +833,7 @@ BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup) BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 0u); } - TestUnloadWallet(std::move(wallet)); + TestUnloadWallet(context, std::move(wallet)); } /* --------------------------- Dash-specific tests start here --------------------------- */ @@ -838,10 +844,11 @@ constexpr CAmount fallbackFee = 1000; // Verify getaddressinfo RPC produces more or less expected results BOOST_FIXTURE_TEST_CASE(rpc_getaddressinfo, TestChain100Setup) { + WalletContext context{m_node.coinjoin_loader}; + context.chain = m_node.chain.get(); std::shared_ptr wallet = std::make_shared(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateMockWalletDatabase()); wallet->SetupLegacyScriptPubKeyMan(); - AddWallet(wallet); - CoreContext context{m_node}; + AddWallet(context, wallet); JSONRPCRequest request; request.context = context; UniValue response; @@ -909,7 +916,7 @@ BOOST_FIXTURE_TEST_CASE(rpc_getaddressinfo, TestChain100Setup) BOOST_CHECK_EQUAL(addresses[1].get_str(), addr2); BOOST_CHECK_EQUAL(pubkeys.size(), 2); - RemoveWallet(wallet, std::nullopt); + RemoveWallet(context, wallet, /*load_on_startup=*/std::nullopt); } class CreateTransactionTestSetup : public TestChain100Setup @@ -936,11 +943,13 @@ class CreateTransactionTestSetup : public TestChain100Setup const std::string strMaxFeeExceeded = "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)"; CreateTransactionTestSetup() + : context{m_node.coinjoin_loader} { + context.chain = m_node.chain.get(); CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); wallet = std::make_unique(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateMockWalletDatabase()); wallet->LoadWallet(); - AddWallet(wallet); + AddWallet(context, wallet); AddKey(*wallet, coinbaseKey); WalletRescanReserver reserver(*wallet); reserver.reserve(); @@ -954,11 +963,12 @@ class CreateTransactionTestSetup : public TestChain100Setup ~CreateTransactionTestSetup() { - RemoveWallet(wallet, std::nullopt); + RemoveWallet(context, wallet, /*load_on_startup=*/std::nullopt); } - std::shared_ptr wallet; CCoinControl coinControl; + WalletContext context; + std::shared_ptr wallet; template bool CheckEqual(const T expected, const T actual) @@ -1021,7 +1031,6 @@ class CreateTransactionTestSetup : public TestChain100Setup std::vector GetRecipients(const std::vector>& vecEntries) { - CoreContext context{m_node}; std::vector vecRecipients; for (auto entry : vecEntries) { JSONRPCRequest request; diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index b762dad809f2e..c9d6bc930dd45 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -63,10 +63,6 @@ const std::map WALLET_FLAG_CAVEATS{ }, }; -RecursiveMutex cs_wallets; -static std::vector> vpwallets GUARDED_BY(cs_wallets); -static std::list g_load_wallet_fns GUARDED_BY(cs_wallets); - bool AddWalletSetting(interfaces::Chain& chain, const std::string& wallet_name) { util::SettingsValue setting_value = chain.getRwSetting("wallet"); @@ -113,14 +109,14 @@ static void RefreshMempoolStatus(CWalletTx& tx, interfaces::Chain& chain) tx.fInMempool = chain.isInMempool(tx.GetHash()); } -bool AddWallet(const std::shared_ptr& wallet) +bool AddWallet(WalletContext& context, const std::shared_ptr& wallet) { { - LOCK(cs_wallets); + LOCK(context.wallets_mutex); assert(wallet); - std::vector>::const_iterator i = std::find(vpwallets.begin(), vpwallets.end(), wallet); - if (i != vpwallets.end()) return false; - vpwallets.push_back(wallet); + std::vector>::const_iterator i = std::find(context.wallets.begin(), context.wallets.end(), wallet); + if (i != context.wallets.end()) return false; + context.wallets.push_back(wallet); } wallet->ConnectScriptPubKeyManNotifiers(); wallet->AutoLockMasternodeCollaterals(); @@ -129,7 +125,7 @@ bool AddWallet(const std::shared_ptr& wallet) return true; } -bool RemoveWallet(const std::shared_ptr& wallet, std::optional load_on_start, std::vector& warnings) +bool RemoveWallet(WalletContext& context, const std::shared_ptr& wallet, std::optional load_on_start, std::vector& warnings) { assert(wallet); @@ -139,10 +135,10 @@ bool RemoveWallet(const std::shared_ptr& wallet, std::optional lo // Unregister with the validation interface which also drops shared pointers. wallet->m_chain_notifications_handler.reset(); { - LOCK(cs_wallets); - std::vector>::iterator i = std::find(vpwallets.begin(), vpwallets.end(), wallet); - if (i == vpwallets.end()) return false; - vpwallets.erase(i); + LOCK(context.wallets_mutex); + std::vector>::iterator i = std::find(context.wallets.begin(), context.wallets.end(), wallet); + if (i == context.wallets.end()) return false; + context.wallets.erase(i); } wallet->coinjoin_loader().RemoveWallet(name); @@ -153,32 +149,32 @@ bool RemoveWallet(const std::shared_ptr& wallet, std::optional lo return true; } -bool RemoveWallet(const std::shared_ptr& wallet, std::optional load_on_start) +bool RemoveWallet(WalletContext& context, const std::shared_ptr& wallet, std::optional load_on_start) { std::vector warnings; - return RemoveWallet(wallet, load_on_start, warnings); + return RemoveWallet(context, wallet, load_on_start, warnings); } -std::vector> GetWallets() +std::vector> GetWallets(WalletContext& context) { - LOCK(cs_wallets); - return vpwallets; + LOCK(context.wallets_mutex); + return context.wallets; } -std::shared_ptr GetWallet(const std::string& name) +std::shared_ptr GetWallet(WalletContext& context, const std::string& name) { - LOCK(cs_wallets); - for (const std::shared_ptr& wallet : vpwallets) { + LOCK(context.wallets_mutex); + for (const std::shared_ptr& wallet : context.wallets) { if (wallet->GetName() == name) return wallet; } return nullptr; } -std::unique_ptr HandleLoadWallet(LoadWalletFn load_wallet) +std::unique_ptr HandleLoadWallet(WalletContext& context, LoadWalletFn load_wallet) { - LOCK(cs_wallets); - auto it = g_load_wallet_fns.emplace(g_load_wallet_fns.end(), std::move(load_wallet)); - return interfaces::MakeHandler([it] { LOCK(cs_wallets); g_load_wallet_fns.erase(it); }); + LOCK(context.wallets_mutex); + auto it = context.wallet_load_fns.emplace(context.wallet_load_fns.end(), std::move(load_wallet)); + return interfaces::MakeHandler([&context, it] { LOCK(context.wallets_mutex); context.wallet_load_fns.erase(it); }); } static Mutex g_loading_wallet_mutex; @@ -246,7 +242,7 @@ std::shared_ptr LoadWalletInternal(WalletContext& context, const std::s status = DatabaseStatus::FAILED_LOAD; return nullptr; } - AddWallet(wallet); + AddWallet(context, wallet); wallet->postInitProcess(); // Write the wallet setting @@ -358,7 +354,7 @@ std::shared_ptr CreateWallet(WalletContext& context, const std::string& wallet->Lock(); } } - AddWallet(wallet); + AddWallet(context, wallet); wallet->postInitProcess(); // Write the wallet settings @@ -2138,9 +2134,9 @@ void CWallet::ResendWalletTransactions() /** @} */ // end of mapWallet -void MaybeResendWalletTxs() +void MaybeResendWalletTxs(WalletContext& context) { - for (const std::shared_ptr& pwallet : GetWallets()) { + for (const std::shared_ptr& pwallet : GetWallets(context)) { pwallet->ResendWalletTransactions(); } } @@ -3533,9 +3529,9 @@ std::shared_ptr CWallet::Create(WalletContext& context, const std::stri } { - LOCK(cs_wallets); - for (auto& load_wallet : g_load_wallet_fns) { - load_wallet(interfaces::MakeWallet(walletInstance)); + LOCK(context.wallets_mutex); + for (auto& load_wallet : context.wallet_load_fns) { + load_wallet(interfaces::MakeWallet(context, walletInstance)); } } diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 59d122d0cc8bb..9ce7f0a04fc95 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -60,15 +60,15 @@ using LoadWalletFn = std::function wall // by the shared pointer deleter. void UnloadWallet(std::shared_ptr&& wallet); -bool AddWallet(const std::shared_ptr& wallet); -bool RemoveWallet(const std::shared_ptr& wallet, std::optional load_on_start, std::vector& warnings); -bool RemoveWallet(const std::shared_ptr& wallet, std::optional load_on_start); -std::vector> GetWallets(); -std::shared_ptr GetWallet(const std::string& name); +bool AddWallet(WalletContext& context, const std::shared_ptr& wallet); +bool RemoveWallet(WalletContext& context, const std::shared_ptr& wallet, std::optional load_on_start, std::vector& warnings); +bool RemoveWallet(WalletContext& context, const std::shared_ptr& wallet, std::optional load_on_start); +std::vector> GetWallets(WalletContext& context); +std::shared_ptr GetWallet(WalletContext& context, const std::string& name); std::shared_ptr LoadWallet(WalletContext& context, const std::string& name, std::optional load_on_start, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector& warnings); std::shared_ptr CreateWallet(WalletContext& context, const std::string& name, std::optional load_on_start, DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector& warnings); std::shared_ptr RestoreWallet(WalletContext& context, const fs::path& backup_file, const std::string& wallet_name, std::optional load_on_start, DatabaseStatus& status, bilingual_str& error, std::vector& warnings); -std::unique_ptr HandleLoadWallet(LoadWalletFn load_wallet); +std::unique_ptr HandleLoadWallet(WalletContext& context, LoadWalletFn load_wallet); std::unique_ptr MakeWalletDatabase(const std::string& name, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error); //! -paytxfee default @@ -1071,7 +1071,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati * Called periodically by the schedule thread. Prompts individual wallets to resend * their transactions. Actual rebroadcast schedule is managed by the wallets themselves. */ -void MaybeResendWalletTxs(); +void MaybeResendWalletTxs(WalletContext& context); /** RAII object to check and reserve a wallet rescan */ class WalletRescanReserver diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index 028c41c1e73de..2f562cbc88c2c 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -1020,14 +1020,14 @@ DBErrors WalletBatch::ZapSelectTx(std::vector& vTxHashIn, std::vector fOneThread(false); if (fOneThread.exchange(true)) { return; } - for (const std::shared_ptr& pwallet : GetWallets()) { + for (const std::shared_ptr& pwallet : GetWallets(context)) { WalletDatabase& dbh = pwallet->GetDatabase(); unsigned int nUpdateCounter = dbh.nUpdateCounter; diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h index 8a980405ef139..bf40a5403eb97 100644 --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -30,6 +30,7 @@ static const bool DEFAULT_FLUSHWALLET = true; struct CBlockLocator; +struct WalletContext; class CHDChain; class CHDPubKey; class CKeyPool; @@ -254,7 +255,7 @@ class WalletBatch }; //! Compacts BDB state so that wallet.dat is self-contained (if there are changes) -void MaybeCompactWalletDB(); +void MaybeCompactWalletDB(WalletContext& context); //! Callback for filtering key types to deserialize in ReadKeyValue using KeyFilterFn = std::function; From 96f43746e6fe49243b4c3a5ac670727573eeab34 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 22 Aug 2021 16:50:36 +0200 Subject: [PATCH 12/16] merge bitcoin#22183: Remove `gArgs` from `wallet.h` and `wallet.cpp` --- src/bitcoin-cli.cpp | 2 +- src/wallet/load.cpp | 11 +++-- src/wallet/test/coinjoin_tests.cpp | 1 + src/wallet/test/wallet_tests.cpp | 8 ++++ src/wallet/wallet.cpp | 67 +++++++++++++++--------------- 5 files changed, 51 insertions(+), 38 deletions(-) diff --git a/src/bitcoin-cli.cpp b/src/bitcoin-cli.cpp index 82b098ce9906d..7eb4237d44b57 100644 --- a/src/bitcoin-cli.cpp +++ b/src/bitcoin-cli.cpp @@ -950,7 +950,7 @@ static void GetWalletBalances(UniValue& result) } /** - * GetProgressBar contructs a progress bar with 5% intervals. + * GetProgressBar constructs a progress bar with 5% intervals. * * @param[in] progress The proportion of the progress bar to be filled between 0 and 1. * @param[out] progress_bar String representation of the progress bar. diff --git a/src/wallet/load.cpp b/src/wallet/load.cpp index c06e8184ddf33..2c9b15908d95d 100644 --- a/src/wallet/load.cpp +++ b/src/wallet/load.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -25,8 +26,10 @@ bool VerifyWallets(WalletContext& context) { interfaces::Chain& chain = *context.chain; - if (gArgs.IsArgSet("-walletdir")) { - const fs::path wallet_dir{gArgs.GetPathArg("-walletdir")}; + ArgsManager& args = *Assert(context.args); + + if (args.IsArgSet("-walletdir")) { + const fs::path wallet_dir{args.GetPathArg("-walletdir")}; std::error_code error; // The canonical path cleans the path, preventing >1 Berkeley environment instances for the same directory // It also lets the fs::exists and fs::is_directory checks below pass on windows, since they return false @@ -43,7 +46,7 @@ bool VerifyWallets(WalletContext& context) chain.initError(strprintf(_("Specified -walletdir \"%s\" is a relative path"), fs::PathToString(wallet_dir))); return false; } - gArgs.ForceSetArg("-walletdir", fs::PathToString(canonical_wallet_dir)); + args.ForceSetArg("-walletdir", fs::PathToString(canonical_wallet_dir)); } LogPrintf("Using wallet directory %s\n", fs::PathToString(GetWalletDir())); @@ -52,7 +55,7 @@ bool VerifyWallets(WalletContext& context) // For backwards compatibility if an unnamed top level wallet exists in the // wallets directory, include it in the default list of wallets to load. - if (!gArgs.IsArgSet("wallet")) { + if (!args.IsArgSet("wallet")) { DatabaseOptions options; DatabaseStatus status; bilingual_str error_string; diff --git a/src/wallet/test/coinjoin_tests.cpp b/src/wallet/test/coinjoin_tests.cpp index d59549e4c85ed..9ee668c30603f 100644 --- a/src/wallet/test/coinjoin_tests.cpp +++ b/src/wallet/test/coinjoin_tests.cpp @@ -132,6 +132,7 @@ class CTransactionBuilderTestSetup : public TestChain100Setup CTransactionBuilderTestSetup() : context{m_node.coinjoin_loader} { + context.args = &gArgs; context.chain = m_node.chain.get(); CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); wallet = std::make_unique(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateMockWalletDatabase()); diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index 3998758b6fb7f..0eecc71b039b6 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -222,6 +222,7 @@ BOOST_FIXTURE_TEST_CASE(importmulti_rescan, TestChain100Setup) wallet->SetupLegacyScriptPubKeyMan(); WITH_LOCK(wallet->cs_wallet, wallet->SetLastBlockProcessed(newTip->nHeight, newTip->GetBlockHash())); WalletContext context{m_node.coinjoin_loader}; + context.args = &gArgs; AddWallet(context, wallet); UniValue keys; keys.setArray(); @@ -282,6 +283,7 @@ BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup) // Import key into wallet and call dumpwallet to create backup file. { WalletContext context{m_node.coinjoin_loader}; + context.args = &gArgs; std::shared_ptr wallet = std::make_shared(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateDummyWalletDatabase()); { auto spk_man = wallet->GetOrCreateLegacyScriptPubKeyMan(); @@ -309,6 +311,7 @@ BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup) wallet->SetupLegacyScriptPubKeyMan(); WalletContext context{m_node.coinjoin_loader}; + context.args = &gArgs; JSONRPCRequest request; request.context = context; request.params.setArray(); @@ -706,6 +709,7 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup) gArgs.ForceSetArg("-unsafesqlitesync", "1"); // Create new wallet with known key and unload it. WalletContext context{m_node.coinjoin_loader}; + context.args = &gArgs; context.chain = m_node.chain.get(); auto wallet = TestLoadWallet(context); CKey key; @@ -796,6 +800,7 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup) BOOST_FIXTURE_TEST_CASE(CreateWalletWithoutChain, BasicTestingSetup) { WalletContext context{/*coinjoin_loader=*/nullptr}; // TODO: FIX FIX FIX + context.args = &gArgs; auto wallet = TestLoadWallet(context); BOOST_CHECK(wallet); UnloadWallet(std::move(wallet)); @@ -805,6 +810,7 @@ BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup) { gArgs.ForceSetArg("-unsafesqlitesync", "1"); WalletContext context{m_node.coinjoin_loader}; + context.args = &gArgs; context.chain = m_node.chain.get(); auto wallet = TestLoadWallet(context); CKey key; @@ -845,6 +851,7 @@ constexpr CAmount fallbackFee = 1000; BOOST_FIXTURE_TEST_CASE(rpc_getaddressinfo, TestChain100Setup) { WalletContext context{m_node.coinjoin_loader}; + context.args = &gArgs; context.chain = m_node.chain.get(); std::shared_ptr wallet = std::make_shared(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateMockWalletDatabase()); wallet->SetupLegacyScriptPubKeyMan(); @@ -945,6 +952,7 @@ class CreateTransactionTestSetup : public TestChain100Setup CreateTransactionTestSetup() : context{m_node.coinjoin_loader} { + context.args = &gArgs; context.chain = m_node.chain.get(); CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); wallet = std::make_unique(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateMockWalletDatabase()); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index c9d6bc930dd45..2313456b631db 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -3267,6 +3267,7 @@ std::shared_ptr CWallet::Create(WalletContext& context, const std::stri { interfaces::Chain* chain = context.chain; interfaces::CoinJoin::Loader* coinjoin_loader = context.coinjoin_loader.get(); + ArgsManager& args = *Assert(context.args); const std::string& walletFile = database->Filename(); const auto start{SteadyClock::now()}; @@ -3322,14 +3323,14 @@ std::shared_ptr CWallet::Create(WalletContext& context, const std::stri if (!(wallet_creation_flags & (WALLET_FLAG_DISABLE_PRIVATE_KEYS | WALLET_FLAG_BLANK_WALLET))) { // Create new HD chain - if (gArgs.GetBoolArg("-usehd", DEFAULT_USE_HD_WALLET) && !walletInstance->IsHDEnabled()) { - std::string strSeed = gArgs.GetArg("-hdseed", "not hex"); + if (args.GetBoolArg("-usehd", DEFAULT_USE_HD_WALLET) && !walletInstance->IsHDEnabled()) { + std::string strSeed = args.GetArg("-hdseed", "not hex"); // ensure this wallet.dat can only be opened by clients supporting HD walletInstance->WalletLogPrintf("Upgrading wallet to HD\n"); walletInstance->SetMinVersion(FEATURE_HD); - if (gArgs.IsArgSet("-hdseed") && IsHex(strSeed)) { + if (args.IsArgSet("-hdseed") && IsHex(strSeed)) { CHDChain newHdChain; std::vector vchSeed = ParseHex(strSeed); if (!newHdChain.SetSeed(SecureVector(vchSeed.begin(), vchSeed.end()), true)) { @@ -3346,12 +3347,12 @@ std::shared_ptr CWallet::Create(WalletContext& context, const std::stri // add default account newHdChain.AddAccount(); } else { - if (gArgs.IsArgSet("-hdseed") && !IsHex(strSeed)) { + if (args.IsArgSet("-hdseed") && !IsHex(strSeed)) { error = strprintf(_("%s -- Incorrect seed, it should be a hex string"), __func__); return nullptr; } - SecureString secureMnemonic = gArgs.GetArg("-mnemonic", "").c_str(); - SecureString secureMnemonicPassphrase = gArgs.GetArg("-mnemonicpassphrase", "").c_str(); + SecureString secureMnemonic = args.GetArg("-mnemonic", "").c_str(); + SecureString secureMnemonicPassphrase = args.GetArg("-mnemonicpassphrase", "").c_str(); LOCK(walletInstance->cs_wallet); if (auto spk_man = walletInstance->GetLegacyScriptPubKeyMan()) { spk_man->GenerateNewHDChain(secureMnemonic, secureMnemonicPassphrase); @@ -3359,9 +3360,9 @@ std::shared_ptr CWallet::Create(WalletContext& context, const std::stri } // clean up - gArgs.ForceRemoveArg("hdseed"); - gArgs.ForceRemoveArg("mnemonic"); - gArgs.ForceRemoveArg("mnemonicpassphrase"); + args.ForceRemoveArg("hdseed"); + args.ForceRemoveArg("mnemonic"); + args.ForceRemoveArg("mnemonicpassphrase"); } // Otherwise, do not create a new HD chain LOCK(walletInstance->cs_wallet); @@ -3402,8 +3403,8 @@ std::shared_ptr CWallet::Create(WalletContext& context, const std::stri } } } - else if (gArgs.IsArgSet("-usehd")) { - bool useHD = gArgs.GetBoolArg("-usehd", DEFAULT_USE_HD_WALLET); + else if (args.IsArgSet("-usehd")) { + bool useHD = args.GetBoolArg("-usehd", DEFAULT_USE_HD_WALLET); if (walletInstance->IsHDEnabled() && !useHD) { error = strprintf(_("Error loading %s: You can't disable HD on an already existing HD wallet"), walletInstance->GetName()); return nullptr; @@ -3419,10 +3420,10 @@ std::shared_ptr CWallet::Create(WalletContext& context, const std::stri SetMiscWarning(_("Make sure to encrypt your wallet and delete all non-encrypted backups after you have verified that the wallet works!")); } - if (gArgs.IsArgSet("-mintxfee")) { - std::optional min_tx_fee = ParseMoney(gArgs.GetArg("-mintxfee", "")); + if (args.IsArgSet("-mintxfee")) { + std::optional min_tx_fee = ParseMoney(args.GetArg("-mintxfee", "")); if (!min_tx_fee || min_tx_fee.value() == 0) { - error = AmountErrMsg("mintxfee", gArgs.GetArg("-mintxfee", "")); + error = AmountErrMsg("mintxfee", args.GetArg("-mintxfee", "")); return nullptr; } else if (min_tx_fee.value() > HIGH_TX_FEE_PER_KB) { warnings.push_back(AmountHighWarn("-mintxfee") + Untranslated(" ") + @@ -3432,8 +3433,8 @@ std::shared_ptr CWallet::Create(WalletContext& context, const std::stri walletInstance->m_min_fee = CFeeRate{min_tx_fee.value()}; } - if (gArgs.IsArgSet("-maxapsfee")) { - const std::string max_aps_fee{gArgs.GetArg("-maxapsfee", "")}; + if (args.IsArgSet("-maxapsfee")) { + const std::string max_aps_fee{args.GetArg("-maxapsfee", "")}; if (max_aps_fee == "-1") { walletInstance->m_max_aps_fee = -1; } else if (std::optional max_fee = ParseMoney(max_aps_fee)) { @@ -3448,10 +3449,10 @@ std::shared_ptr CWallet::Create(WalletContext& context, const std::stri } } - if (gArgs.IsArgSet("-fallbackfee")) { - std::optional fallback_fee = ParseMoney(gArgs.GetArg("-fallbackfee", "")); + if (args.IsArgSet("-fallbackfee")) { + std::optional fallback_fee = ParseMoney(args.GetArg("-fallbackfee", "")); if (!fallback_fee) { - error = strprintf(_("Invalid amount for -fallbackfee=: '%s'"), gArgs.GetArg("-fallbackfee", "")); + error = strprintf(_("Invalid amount for -fallbackfee=: '%s'"), args.GetArg("-fallbackfee", "")); return nullptr; } else if (fallback_fee.value() > HIGH_TX_FEE_PER_KB) { warnings.push_back(AmountHighWarn("-fallbackfee") + Untranslated(" ") + @@ -3462,10 +3463,10 @@ std::shared_ptr CWallet::Create(WalletContext& context, const std::stri // Disable fallback fee in case value was set to 0, enable if non-null value walletInstance->m_allow_fallback_fee = walletInstance->m_fallback_fee.GetFeePerK() != 0; - if (gArgs.IsArgSet("-discardfee")) { - std::optional discard_fee = ParseMoney(gArgs.GetArg("-discardfee", "")); + if (args.IsArgSet("-discardfee")) { + std::optional discard_fee = ParseMoney(args.GetArg("-discardfee", "")); if (!discard_fee) { - error = strprintf(_("Invalid amount for -discardfee=: '%s'"), gArgs.GetArg("-discardfee", "")); + error = strprintf(_("Invalid amount for -discardfee=: '%s'"), args.GetArg("-discardfee", "")); return nullptr; } else if (discard_fee.value() > HIGH_TX_FEE_PER_KB) { warnings.push_back(AmountHighWarn("-discardfee") + Untranslated(" ") + @@ -3474,10 +3475,10 @@ std::shared_ptr CWallet::Create(WalletContext& context, const std::stri walletInstance->m_discard_rate = CFeeRate{discard_fee.value()}; } - if (gArgs.IsArgSet("-paytxfee")) { - std::optional pay_tx_fee = ParseMoney(gArgs.GetArg("-paytxfee", "")); + if (args.IsArgSet("-paytxfee")) { + std::optional pay_tx_fee = ParseMoney(args.GetArg("-paytxfee", "")); if (!pay_tx_fee) { - error = AmountErrMsg("paytxfee", gArgs.GetArg("-paytxfee", "")); + error = AmountErrMsg("paytxfee", args.GetArg("-paytxfee", "")); return nullptr; } else if (pay_tx_fee.value() > HIGH_TX_FEE_PER_KB) { warnings.push_back(AmountHighWarn("-paytxfee") + Untranslated(" ") + @@ -3486,22 +3487,22 @@ std::shared_ptr CWallet::Create(WalletContext& context, const std::stri walletInstance->m_pay_tx_fee = CFeeRate{pay_tx_fee.value(), 1000}; if (chain && walletInstance->m_pay_tx_fee < chain->relayMinFee()) { error = strprintf(_("Invalid amount for -paytxfee=: '%s' (must be at least %s)"), - gArgs.GetArg("-paytxfee", ""), chain->relayMinFee().ToString()); + args.GetArg("-paytxfee", ""), chain->relayMinFee().ToString()); return nullptr; } } - if (gArgs.IsArgSet("-maxtxfee")) { - std::optional max_fee = ParseMoney(gArgs.GetArg("-maxtxfee", "")); + if (args.IsArgSet("-maxtxfee")) { + std::optional max_fee = ParseMoney(args.GetArg("-maxtxfee", "")); if (!max_fee) { - error = AmountErrMsg("maxtxfee", gArgs.GetArg("-maxtxfee", "")); + error = AmountErrMsg("maxtxfee", args.GetArg("-maxtxfee", "")); return nullptr; } else if (max_fee.value() > HIGH_MAX_TX_FEE) { warnings.push_back(_("-maxtxfee is set very high! Fees this large could be paid on a single transaction.")); } if (chain && CFeeRate{max_fee.value(), 1000} < chain->relayMinFee()) { error = strprintf(_("Invalid amount for -maxtxfee=: '%s' (must be at least the minrelay fee of %s to prevent stuck transactions)"), - gArgs.GetArg("-maxtxfee", ""), chain->relayMinFee().ToString()); + args.GetArg("-maxtxfee", ""), chain->relayMinFee().ToString()); return nullptr; } @@ -3512,8 +3513,8 @@ std::shared_ptr CWallet::Create(WalletContext& context, const std::stri warnings.push_back(AmountHighWarn("-minrelaytxfee") + Untranslated(" ") + _("The wallet will avoid paying less than the minimum relay fee.")); - walletInstance->m_confirm_target = gArgs.GetArg("-txconfirmtarget", DEFAULT_TX_CONFIRM_TARGET); - walletInstance->m_spend_zero_conf_change = gArgs.GetBoolArg("-spendzeroconfchange", DEFAULT_SPEND_ZEROCONF_CHANGE); + walletInstance->m_confirm_target = args.GetArg("-txconfirmtarget", DEFAULT_TX_CONFIRM_TARGET); + walletInstance->m_spend_zero_conf_change = args.GetBoolArg("-spendzeroconfchange", DEFAULT_SPEND_ZEROCONF_CHANGE); walletInstance->WalletLogPrintf("Wallet completed loading in %15dms\n", Ticks(SteadyClock::now() - start)); @@ -3538,7 +3539,7 @@ std::shared_ptr CWallet::Create(WalletContext& context, const std::stri { LOCK(walletInstance->cs_wallet); - walletInstance->SetBroadcastTransactions(gArgs.GetBoolArg("-walletbroadcast", DEFAULT_WALLETBROADCAST)); + walletInstance->SetBroadcastTransactions(args.GetBoolArg("-walletbroadcast", DEFAULT_WALLETBROADCAST)); walletInstance->WalletLogPrintf("setExternalKeyPool.size() = %u\n", walletInstance->KeypoolCountExternalKeys()); walletInstance->WalletLogPrintf("GetKeyPoolSize() = %u\n", walletInstance->GetKeyPoolSize()); walletInstance->WalletLogPrintf("mapWallet.size() = %u\n", walletInstance->mapWallet.size()); From 87506f8072bfbf5e97cd1eb00edc32f01e03370d Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Tue, 7 Jan 2025 08:33:26 +0000 Subject: [PATCH 13/16] merge bitcoin#22009: Decide which coin selection solution to use based on waste metric --- src/dummywallet.cpp | 1 + src/wallet/coinselection.cpp | 27 ++++ src/wallet/coinselection.h | 15 +++ src/wallet/init.cpp | 1 + src/wallet/spend.cpp | 41 ++++-- src/wallet/test/coinselector_tests.cpp | 171 +++++++++++++++++++------ src/wallet/wallet.cpp | 9 ++ src/wallet/wallet.h | 8 ++ 8 files changed, 224 insertions(+), 49 deletions(-) diff --git a/src/dummywallet.cpp b/src/dummywallet.cpp index 6e5c59b10bafc..77b43a851d0e0 100644 --- a/src/dummywallet.cpp +++ b/src/dummywallet.cpp @@ -33,6 +33,7 @@ void DummyWalletInit::AddWalletOptions(ArgsManager& argsman) const { argsman.AddHiddenArgs({ "-avoidpartialspends", + "-consolidatefeerate=", "-createwalletbackups=", "-disablewallet", "-instantsendnotify=", diff --git a/src/wallet/coinselection.cpp b/src/wallet/coinselection.cpp index 0a908e934debf..f34c605583136 100644 --- a/src/wallet/coinselection.cpp +++ b/src/wallet/coinselection.cpp @@ -402,3 +402,30 @@ CAmount OutputGroup::GetSelectionAmount() const { return m_subtract_fee_outputs ? m_value : effective_value; } + +CAmount GetSelectionWaste(const std::set& inputs, CAmount change_cost, CAmount target, bool use_effective_value) +{ + // This function should not be called with empty inputs as that would mean the selection failed + assert(!inputs.empty()); + + // Always consider the cost of spending an input now vs in the future. + CAmount waste = 0; + CAmount selected_effective_value = 0; + for (const CInputCoin& coin : inputs) { + waste += coin.m_fee - coin.m_long_term_fee; + selected_effective_value += use_effective_value ? coin.effective_value : coin.txout.nValue; + } + + if (change_cost) { + // Consider the cost of making change and spending it in the future + // If we aren't making change, the caller should've set change_cost to 0 + assert(change_cost > 0); + waste += change_cost; + } else { + // When we are not making change (change_cost == 0), consider the excess we are throwing away to fees + assert(selected_effective_value >= target); + waste += selected_effective_value - target; + } + + return waste; +} diff --git a/src/wallet/coinselection.h b/src/wallet/coinselection.h index cbe80d87c682b..14b6a371ba2de 100644 --- a/src/wallet/coinselection.h +++ b/src/wallet/coinselection.h @@ -164,6 +164,21 @@ struct OutputGroup CAmount GetSelectionAmount() const; }; +/** Compute the waste for this result given the cost of change + * and the opportunity cost of spending these inputs now vs in the future. + * If change exists, waste = change_cost + inputs * (effective_feerate - long_term_feerate) + * If no change, waste = excess + inputs * (effective_feerate - long_term_feerate) + * where excess = selected_effective_value - target + * change_cost = effective_feerate * change_output_size + long_term_feerate * change_spend_size + * + * @param[in] inputs The selected inputs + * @param[in] change_cost The cost of creating change and spending it in the future. Only used if there is change. Must be 0 if there is no change. + * @param[in] target The amount targeted by the coin selection algorithm. + * @param[in] use_effective_value Whether to use the input's effective value (when true) or the real value (when false). + * @return The waste + */ +[[nodiscard]] CAmount GetSelectionWaste(const std::set& inputs, CAmount change_cost, CAmount target, bool use_effective_value = true); + bool SelectCoinsBnB(std::vector& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change, std::set& out_set, CAmount& value_ret); // Original coin selection algorithm as a fallback diff --git a/src/wallet/init.cpp b/src/wallet/init.cpp index 66a8c8a33f73e..9ca45620b7854 100644 --- a/src/wallet/init.cpp +++ b/src/wallet/init.cpp @@ -55,6 +55,7 @@ const WalletInitInterface& g_wallet_init_interface = WalletInit(); void WalletInit::AddWalletOptions(ArgsManager& argsman) const { argsman.AddArg("-avoidpartialspends", strprintf("Group outputs by address, selecting many (possibly all) or none, instead of selecting on a per-output basis. Privacy is improved as addresses are mostly swept with fewer transactions and outputs are aggregated in clean change addresses. It may result in higher fees due to less optimal coin selection caused by this added limitation and possibly a larger-than-necessary number of inputs being used. Always enabled for wallets with \"avoid_reuse\" enabled, otherwise default: %u.", DEFAULT_AVOIDPARTIALSPENDS), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET); + argsman.AddArg("-consolidatefeerate=", strprintf("The maximum feerate (in %s/kvB) at which transaction building may use more inputs than strictly necessary so that the wallet's UTXO pool can be reduced (default: %s).", CURRENCY_UNIT, FormatMoney(DEFAULT_CONSOLIDATE_FEERATE)), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET); argsman.AddArg("-createwalletbackups=", strprintf("Number of automatic wallet backups (default: %u)", nWalletBackups), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET); argsman.AddArg("-disablewallet", "Do not load the wallet and disable wallet RPC calls", ArgsManager::ALLOW_ANY, OptionsCategory::WALLET); #if HAVE_SYSTEM diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp index 6901caaa1179a..da879c63cafb0 100644 --- a/src/wallet/spend.cpp +++ b/src/wallet/spend.cpp @@ -366,17 +366,44 @@ bool CWallet::AttemptSelection(const CAmount& nTargetValue, const CoinEligibilit { setCoinsRet.clear(); nValueRet = 0; + // Vector of results for use with waste calculation + // In order: calculated waste, selected inputs, selected input value (sum of input values) + // TODO: Use a struct representing the selection result + std::vector, CAmount>> results; // Note that unlike KnapsackSolver, we do not include the fee for creating a change output as BnB will not create a change output. std::vector positive_groups = GroupOutputs(coins, coin_selection_params, eligibility_filter, true /* positive_only */); - if (SelectCoinsBnB(positive_groups, nTargetValue, coin_selection_params.m_cost_of_change, setCoinsRet, nValueRet)) { - return true; + std::set bnb_coins; + CAmount bnb_value; + if (SelectCoinsBnB(positive_groups, nTargetValue, coin_selection_params.m_cost_of_change, bnb_coins, bnb_value)) { + const auto waste = GetSelectionWaste(bnb_coins, /* cost of change */ CAmount(0), nTargetValue, !coin_selection_params.m_subtract_fee_outputs); + results.emplace_back(std::make_tuple(waste, std::move(bnb_coins), bnb_value)); } + // The knapsack solver has some legacy behavior where it will spend dust outputs. We retain this behavior, so don't filter for positive only here. std::vector all_groups = GroupOutputs(coins, coin_selection_params, eligibility_filter, false /* positive_only */); // While nTargetValue includes the transaction fees for non-input things, it does not include the fee for creating a change output. // So we need to include that for KnapsackSolver as well, as we are expecting to create a change output. - return KnapsackSolver(nTargetValue + coin_selection_params.m_change_fee, all_groups, setCoinsRet, nValueRet, nCoinType == CoinType::ONLY_FULLY_MIXED, m_default_max_tx_fee); + std::set knapsack_coins; + CAmount knapsack_value; + if (KnapsackSolver(nTargetValue + coin_selection_params.m_change_fee, all_groups, knapsack_coins, knapsack_value, nCoinType == CoinType::ONLY_FULLY_MIXED, m_default_max_tx_fee)) { + const auto waste = GetSelectionWaste(knapsack_coins, coin_selection_params.m_cost_of_change, nTargetValue + coin_selection_params.m_change_fee, !coin_selection_params.m_subtract_fee_outputs); + results.emplace_back(std::make_tuple(waste, std::move(knapsack_coins), knapsack_value)); + } + + if (results.size() == 0) { + // No solution found + return false; + } + + // Choose the result with the least waste + // If the waste is the same, choose the one which spends more inputs. + const auto& best_result = std::min_element(results.begin(), results.end(), [](const auto& a, const auto& b) { + return std::get<0>(a) < std::get<0>(b) || (std::get<0>(a) == std::get<0>(b) && std::get<1>(a).size() > std::get<1>(b).size()); + }); + setCoinsRet = std::get<1>(*best_result); + nValueRet = std::get<2>(*best_result); + return true; } bool CWallet::SelectCoins(const std::vector& vAvailableCoins, const CAmount& nTargetValue, std::set& setCoinsRet, CAmount& nValueRet, const CCoinControl& coin_control, CoinSelectionParams& coin_selection_params) const @@ -610,6 +637,9 @@ bool CWallet::CreateTransactionInternal( CoinSelectionParams coin_selection_params; // Parameters for coin selection, init with dummy coin_selection_params.m_avoid_partial_spends = coin_control.m_avoid_partial_spends; + // Set the long term feerate estimate to the wallet's consolidate feerate + coin_selection_params.m_long_term_feerate = m_consolidate_feerate; + CAmount recipients_sum = 0; ReserveDestination reservedest(this); const bool sort_bip69{nChangePosInOut == -1}; @@ -682,11 +712,6 @@ bool CWallet::CreateTransactionInternal( return false; } - // Get long term estimate - CCoinControl cc_temp; - cc_temp.m_confirm_target = chain().estimateMaxBlocks(); - coin_selection_params.m_long_term_feerate = GetMinimumFeeRate(*this, cc_temp, nullptr); - // Calculate the cost of change // Cost of change is the cost of creating the change output + cost of spending the change output in the future. // For creating the change output now, we use the effective feerate. diff --git a/src/wallet/test/coinselector_tests.cpp b/src/wallet/test/coinselector_tests.cpp index f59f587092a4b..2abdc8a2b2e73 100644 --- a/src/wallet/test/coinselector_tests.cpp +++ b/src/wallet/test/coinselector_tests.cpp @@ -47,12 +47,16 @@ static void add_coin(const CAmount& nValue, int nInput, std::vector& set.emplace_back(MakeTransactionRef(tx), nInput); } -static void add_coin(const CAmount& nValue, int nInput, CoinSet& set) +static void add_coin(const CAmount& nValue, int nInput, CoinSet& set, CAmount fee = 0, CAmount long_term_fee = 0) { CMutableTransaction tx; tx.vout.resize(nInput + 1); tx.vout[nInput].nValue = nValue; - set.emplace(MakeTransactionRef(tx), nInput); + CInputCoin coin(MakeTransactionRef(tx), nInput); + coin.effective_value = nValue - fee; + coin.m_fee = fee; + coin.m_long_term_fee = long_term_fee; + set.insert(coin); } static void add_coin(std::vector& coins, CWallet& wallet, const CAmount& nValue, int nAge = 6*24, bool fIsFromMe = false, int nInput=0, bool spendable = false) @@ -124,6 +128,22 @@ inline std::vector& GroupCoins(const std::vector& coins) return static_groups; } +inline bool KnapsackSolver(const CAmount& nTargetValue, std::vector& groups, std::set& setCoinsRet, CAmount& nValueRet) +{ + return KnapsackSolver(nTargetValue, groups, setCoinsRet, nValueRet, /*fFulyMixedOnly=*/false, /*maxTxFee=*/DEFAULT_TRANSACTION_MAXFEE); +} + +inline std::vector& KnapsackGroupOutputs(const std::vector& coins, CWallet& wallet, const CoinEligibilityFilter& filter) +{ + CoinSelectionParams coin_selection_params(/* change_output_size= */ 0, + /* change_spend_size= */ 0, /* effective_feerate= */ CFeeRate(0), + /* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0), + /* tx_noinputs_size= */ 0, /* avoid_partial= */ false); + static std::vector static_groups; + static_groups = wallet.GroupOutputs(coins, coin_selection_params, filter, /* positive_only */false); + return static_groups; +} + // Branch and bound coin selection tests BOOST_AUTO_TEST_CASE(bnb_search_test) { @@ -271,14 +291,14 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) add_coin(coins, *wallet, 1); coins.at(0).nInputBytes = 40; // Make sure that it has a negative effective value. The next check should assert if this somehow got through. Otherwise it will fail - BOOST_CHECK(!wallet->AttemptSelection(1 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params_bnb)); + BOOST_CHECK(!SelectCoinsBnB(GroupCoins(coins), 1 * CENT, coin_selection_params_bnb.m_cost_of_change, setCoinsRet, nValueRet)); // Test fees subtracted from output: coins.clear(); add_coin(coins, *wallet, 1 * CENT); coins.at(0).nInputBytes = 40; coin_selection_params_bnb.m_subtract_fee_outputs = true; - BOOST_CHECK(wallet->AttemptSelection(1 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params_bnb)); + BOOST_CHECK(SelectCoinsBnB(GroupCoins(coins), 1 * CENT, coin_selection_params_bnb.m_cost_of_change, setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 1 * CENT); } @@ -320,24 +340,24 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) coins.clear(); // with an empty wallet we can't even pay one cent - BOOST_CHECK(!wallet->AttemptSelection( 1 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(!KnapsackSolver(1 * CENT, KnapsackGroupOutputs(coins, *wallet, filter_standard), setCoinsRet, nValueRet)); add_coin(coins, *wallet, 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->AttemptSelection( 1 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(!KnapsackSolver(1 * CENT, KnapsackGroupOutputs(coins, *wallet, filter_standard), setCoinsRet, nValueRet)); // but we can find a new 1 cent - BOOST_CHECK(wallet->AttemptSelection( 1 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(1 * CENT, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 1 * CENT); add_coin(coins, *wallet, 2*CENT); // add a mature 2 cent coin // we can't make 3 cents of mature coins - BOOST_CHECK(!wallet->AttemptSelection( 3 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(!KnapsackSolver(3 * CENT, KnapsackGroupOutputs(coins, *wallet, filter_standard), setCoinsRet, nValueRet)); // we can make 3 cents of new coins - BOOST_CHECK(wallet->AttemptSelection( 3 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(3 * CENT, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 3 * CENT); add_coin(coins, *wallet, 5*CENT); // add a mature 5 cent coin, @@ -347,33 +367,33 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) // 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->AttemptSelection(38 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(!KnapsackSolver(38 * CENT, KnapsackGroupOutputs(coins, *wallet, filter_standard), 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->AttemptSelection(38 * CENT, filter_standard_extra, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(!KnapsackSolver(38 * CENT, KnapsackGroupOutputs(coins, *wallet, filter_standard_extra), setCoinsRet, nValueRet)); // but we can make 37 cents if we accept new coins from ourself - BOOST_CHECK(wallet->AttemptSelection(37 * CENT, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(37 * CENT, KnapsackGroupOutputs(coins, *wallet, filter_standard), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 37 * CENT); // and we can make 38 cents if we accept all new coins - BOOST_CHECK(wallet->AttemptSelection(38 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(38 * CENT, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), 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->AttemptSelection(34 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(34 * CENT, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 35 * CENT); // but 35 cents is closest BOOST_CHECK_EQUAL(setCoinsRet.size(), 3U); // 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. We should see just 2+5 - BOOST_CHECK(wallet->AttemptSelection( 7 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(7 * CENT, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), setCoinsRet, nValueRet)); 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(wallet->AttemptSelection( 8 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(8 * CENT, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), setCoinsRet, nValueRet)); 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(wallet->AttemptSelection( 9 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(9 * CENT, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 10 * CENT); BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); @@ -387,30 +407,30 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) add_coin(coins, *wallet, 30*CENT); // now we have 6+7+8+20+30 = 71 cents total // check that we have 71 and not 72 - BOOST_CHECK(wallet->AttemptSelection(71 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); - BOOST_CHECK(!wallet->AttemptSelection(72 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(71 * CENT, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), setCoinsRet, nValueRet)); + BOOST_CHECK(!KnapsackSolver(72 * CENT, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), 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->AttemptSelection(16 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(16 * CENT, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 20 * CENT); // we should get 20 in one coin BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); add_coin(coins, *wallet, 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->AttemptSelection(16 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(16 * CENT, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 18 * CENT); // we should get 18 in 3 coins BOOST_CHECK_EQUAL(setCoinsRet.size(), 3U); add_coin(coins, *wallet, 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->AttemptSelection(16 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(16 * CENT, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 18 * CENT); // we should get 18 in 1 coin BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); // because in the event of a tie, the biggest coin wins // now try making 11 cents. we should get 5+6 - BOOST_CHECK(wallet->AttemptSelection(11 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(11 * CENT, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 11 * CENT); BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U); @@ -419,11 +439,11 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) add_coin(coins, *wallet, 2*COIN); add_coin(coins, *wallet, 3*COIN); add_coin(coins, *wallet, 4*COIN); // now we have 5+6+7+8+18+20+30+100+200+300+400 = 1094 cents - BOOST_CHECK(wallet->AttemptSelection(95 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(95 * CENT, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 1 * COIN); // we should get 1 BTC in 1 coin BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); - BOOST_CHECK(wallet->AttemptSelection(195 * CENT, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(195 * CENT, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 2 * COIN); // we should get 2 BTC in 1 coin BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); @@ -438,14 +458,14 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) // 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(wallet->AttemptSelection(MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(MIN_CHANGE, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, MIN_CHANGE); // but if we add a bigger coin, small change is avoided add_coin(coins, *wallet, 1111*MIN_CHANGE); // try making 1 from 0.1 + 0.2 + 0.3 + 0.4 + 0.5 + 1111 = 1112.5 - BOOST_CHECK(wallet->AttemptSelection(1 * MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(1 * MIN_CHANGE, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 1 * MIN_CHANGE); // we should get the exact amount // if we add more small coins: @@ -453,7 +473,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) add_coin(coins, *wallet, MIN_CHANGE * 7 / 10); // and try again to make 1.0 * MIN_CHANGE - BOOST_CHECK(wallet->AttemptSelection(1 * MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(1 * MIN_CHANGE, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 1 * MIN_CHANGE); // we should get the exact amount // run the 'mtgox' test (see https://blockexplorer.com/tx/29a3efd3ef04f9153d47a990bd7b048a4b2d213daaa5fb8ed670fb85f13bdbcf) @@ -462,7 +482,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) for (int j = 0; j < 20; j++) add_coin(coins, *wallet, 50000 * COIN); - BOOST_CHECK(wallet->AttemptSelection(500000 * COIN, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(500000 * COIN, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 500000 * COIN); // we should get the exact amount BOOST_CHECK_EQUAL(setCoinsRet.size(), 10U); // in ten coins @@ -475,7 +495,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) add_coin(coins, *wallet, MIN_CHANGE * 6 / 10); add_coin(coins, *wallet, MIN_CHANGE * 7 / 10); add_coin(coins, *wallet, 1111 * MIN_CHANGE); - BOOST_CHECK(wallet->AttemptSelection(1 * MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(1 * MIN_CHANGE, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 1111 * MIN_CHANGE); // we get the bigger coin BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); @@ -485,7 +505,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) add_coin(coins, *wallet, MIN_CHANGE * 6 / 10); add_coin(coins, *wallet, MIN_CHANGE * 8 / 10); add_coin(coins, *wallet, 1111 * MIN_CHANGE); - BOOST_CHECK(wallet->AttemptSelection(MIN_CHANGE, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(MIN_CHANGE, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, MIN_CHANGE); // we should get the exact amount BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U); // in two coins 0.4+0.6 @@ -496,12 +516,12 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) add_coin(coins, *wallet, MIN_CHANGE * 100); // trying to make 100.01 from these three coins - BOOST_CHECK(wallet->AttemptSelection(MIN_CHANGE * 10001 / 100, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(MIN_CHANGE * 10001 / 100, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, MIN_CHANGE * 10105 / 100); // we should get all coins 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(wallet->AttemptSelection(MIN_CHANGE * 9990 / 100, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(MIN_CHANGE * 9990 / 100, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 101 * MIN_CHANGE); BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U); } @@ -515,7 +535,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) // We only create the wallet once to save time, but we still run the coin selection RUN_TESTS times. for (int i = 0; i < RUN_TESTS; i++) { - BOOST_CHECK(wallet->AttemptSelection(2000, filter_confirmed, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(2000, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), setCoinsRet, nValueRet)); if (amt - 2000 < MIN_CHANGE) { // needs more than one input: @@ -541,8 +561,8 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) for (int i = 0; i < RUN_TESTS; i++) { // picking 50 from 100 coins doesn't depend on the shuffle, // but does depend on randomness in the stochastic approximation code - BOOST_CHECK(KnapsackSolver(50 * COIN, GroupCoins(coins), setCoinsRet, nValueRet, /*fFullyMixedOnly=*/false, /*maxTxFee=*/DEFAULT_TRANSACTION_MAXFEE)); - BOOST_CHECK(KnapsackSolver(50 * COIN, GroupCoins(coins), setCoinsRet2, nValueRet, /*fFullyMixedOnly=*/false, /*maxTxFee=*/DEFAULT_TRANSACTION_MAXFEE)); + BOOST_CHECK(KnapsackSolver(50 * COIN, GroupCoins(coins), setCoinsRet, nValueRet)); + BOOST_CHECK(KnapsackSolver(50 * COIN, GroupCoins(coins), setCoinsRet2, nValueRet)); BOOST_CHECK(!equal_sets(setCoinsRet, setCoinsRet2)); int fails = 0; @@ -552,8 +572,8 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) // When choosing 1 from 100 identical coins, 1% of the time, this test will choose the same coin twice // which will cause it to fail. // To avoid that issue, run the test RANDOM_REPEATS times and only complain if all of them fail - BOOST_CHECK(KnapsackSolver(COIN, GroupCoins(coins), setCoinsRet, nValueRet, /*fFullyMixedOnly=*/false, /*maxTxFee=*/DEFAULT_TRANSACTION_MAXFEE)); - BOOST_CHECK(KnapsackSolver(COIN, GroupCoins(coins), setCoinsRet2, nValueRet, /*fFullyMixedOnly=*/false, /*maxTxFee=*/DEFAULT_TRANSACTION_MAXFEE)); + BOOST_CHECK(KnapsackSolver(COIN, GroupCoins(coins), setCoinsRet, nValueRet)); + BOOST_CHECK(KnapsackSolver(COIN, GroupCoins(coins), setCoinsRet2, nValueRet)); if (equal_sets(setCoinsRet, setCoinsRet2)) fails++; } @@ -573,8 +593,8 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) int fails = 0; for (int j = 0; j < RANDOM_REPEATS; j++) { - BOOST_CHECK(KnapsackSolver(90*CENT, GroupCoins(coins), setCoinsRet, nValueRet, /*fFullyMixedOnly=*/false, /*maxTxFee=*/DEFAULT_TRANSACTION_MAXFEE)); - BOOST_CHECK(KnapsackSolver(90*CENT, GroupCoins(coins), setCoinsRet2, nValueRet, /*fFullyMixedOnly=*/false, /*maxTxFee=*/DEFAULT_TRANSACTION_MAXFEE)); + BOOST_CHECK(KnapsackSolver(90*CENT, GroupCoins(coins), setCoinsRet, nValueRet)); + BOOST_CHECK(KnapsackSolver(90*CENT, GroupCoins(coins), setCoinsRet2, nValueRet)); if (equal_sets(setCoinsRet, setCoinsRet2)) fails++; } @@ -599,7 +619,7 @@ BOOST_AUTO_TEST_CASE(ApproximateBestSubset) add_coin(coins, *wallet, 1000 * COIN); add_coin(coins, *wallet, 3 * COIN); - BOOST_CHECK(wallet->AttemptSelection(1003 * COIN, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(1003 * COIN, KnapsackGroupOutputs(coins, *wallet, filter_standard), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 1003 * COIN); BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U); } @@ -650,4 +670,73 @@ BOOST_AUTO_TEST_CASE(SelectCoins_test) } } +BOOST_AUTO_TEST_CASE(waste_test) +{ + CoinSet selection; + const CAmount fee{100}; + const CAmount change_cost{125}; + const CAmount fee_diff{40}; + const CAmount in_amt{3 * COIN}; + const CAmount target{2 * COIN}; + const CAmount excess{in_amt - fee * 2 - target}; + + // Waste with change is the change cost and difference between fee and long term fee + add_coin(1 * COIN, 1, selection, fee, fee - fee_diff); + add_coin(2 * COIN, 2, selection, fee, fee - fee_diff); + const CAmount waste1 = GetSelectionWaste(selection, change_cost, target); + BOOST_CHECK_EQUAL(fee_diff * 2 + change_cost, waste1); + selection.clear(); + + // Waste without change is the excess and difference between fee and long term fee + add_coin(1 * COIN, 1, selection, fee, fee - fee_diff); + add_coin(2 * COIN, 2, selection, fee, fee - fee_diff); + const CAmount waste_nochange1 = GetSelectionWaste(selection, 0, target); + BOOST_CHECK_EQUAL(fee_diff * 2 + excess, waste_nochange1); + selection.clear(); + + // Waste with change and fee == long term fee is just cost of change + add_coin(1 * COIN, 1, selection, fee, fee); + add_coin(2 * COIN, 2, selection, fee, fee); + BOOST_CHECK_EQUAL(change_cost, GetSelectionWaste(selection, change_cost, target)); + selection.clear(); + + // Waste without change and fee == long term fee is just the excess + add_coin(1 * COIN, 1, selection, fee, fee); + add_coin(2 * COIN, 2, selection, fee, fee); + BOOST_CHECK_EQUAL(excess, GetSelectionWaste(selection, 0, target)); + selection.clear(); + + // Waste will be greater when fee is greater, but long term fee is the same + add_coin(1 * COIN, 1, selection, fee * 2, fee - fee_diff); + add_coin(2 * COIN, 2, selection, fee * 2, fee - fee_diff); + const CAmount waste2 = GetSelectionWaste(selection, change_cost, target); + BOOST_CHECK_GT(waste2, waste1); + selection.clear(); + + // Waste with change is the change cost and difference between fee and long term fee + // With long term fee greater than fee, waste should be less than when long term fee is less than fee + add_coin(1 * COIN, 1, selection, fee, fee + fee_diff); + add_coin(2 * COIN, 2, selection, fee, fee + fee_diff); + const CAmount waste3 = GetSelectionWaste(selection, change_cost, target); + BOOST_CHECK_EQUAL(fee_diff * -2 + change_cost, waste3); + BOOST_CHECK_LT(waste3, waste1); + selection.clear(); + + // Waste without change is the excess and difference between fee and long term fee + // With long term fee greater than fee, waste should be less than when long term fee is less than fee + add_coin(1 * COIN, 1, selection, fee, fee + fee_diff); + add_coin(2 * COIN, 2, selection, fee, fee + fee_diff); + const CAmount waste_nochange2 = GetSelectionWaste(selection, 0, target); + BOOST_CHECK_EQUAL(fee_diff * -2 + excess, waste_nochange2); + BOOST_CHECK_LT(waste_nochange2, waste_nochange1); + selection.clear(); + + // 0 Waste only when fee == long term fee, no change, and no excess + add_coin(1 * COIN, 1, selection, fee, fee); + add_coin(2 * COIN, 2, selection, fee, fee); + const CAmount exact_target = in_amt - 2 * fee; + BOOST_CHECK_EQUAL(0, GetSelectionWaste(selection, 0, exact_target)); + +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 2313456b631db..e4ef052d7cc67 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -3509,6 +3509,15 @@ std::shared_ptr CWallet::Create(WalletContext& context, const std::stri walletInstance->m_default_max_tx_fee = max_fee.value(); } + if (gArgs.IsArgSet("-consolidatefeerate")) { + if (std::optional consolidate_feerate = ParseMoney(gArgs.GetArg("-consolidatefeerate", ""))) { + walletInstance->m_consolidate_feerate = CFeeRate(*consolidate_feerate); + } else { + error = AmountErrMsg("consolidatefeerate", gArgs.GetArg("-consolidatefeerate", "")); + return nullptr; + } + } + if (chain && chain->relayMinFee().GetFeePerK() > HIGH_TX_FEE_PER_KB) warnings.push_back(AmountHighWarn("-minrelaytxfee") + Untranslated(" ") + _("The wallet will avoid paying less than the minimum relay fee.")); diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 9ce7f0a04fc95..9741178cbf256 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -79,6 +79,8 @@ static const CAmount DEFAULT_FALLBACK_FEE = 1000; static const CAmount DEFAULT_DISCARD_FEE = 10000; //! -mintxfee default static const CAmount DEFAULT_TRANSACTION_MINFEE = 1000; +//! -consolidatefeerate default +static const CAmount DEFAULT_CONSOLIDATE_FEERATE{10000}; // 10 sat/vbyte /** * maximum fee increase allowed to do partial spend avoidance, even for nodes with this feature disabled by default * @@ -763,6 +765,12 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati * output itself, just drop it to fees. */ CFeeRate m_discard_rate{DEFAULT_DISCARD_FEE}; + /** When the actual feerate is less than the consolidate feerate, we will tend to make transactions which + * consolidate inputs. When the actual feerate is greater than the consolidate feerate, we will tend to make + * transactions which have the lowest fees. + */ + CFeeRate m_consolidate_feerate{DEFAULT_CONSOLIDATE_FEERATE}; + /** The maximum fee amount we're willing to pay to prioritize partial spend avoidance. */ CAmount m_max_aps_fee{DEFAULT_MAX_AVOIDPARTIALSPEND_FEE}; //!< note: this is absolute fee, not fee rate /** Absolute maximum transaction fee (in satoshis) used by default for the wallet */ From 04f63ff230ea74b393556bd41d3328456cd83e07 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Fri, 10 Sep 2021 15:47:37 +0530 Subject: [PATCH 14/16] merge bitcoin#22938: Add remaining scenarios of 0 waste, in wallet waste_test --- src/wallet/test/coinselector_tests.cpp | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/wallet/test/coinselector_tests.cpp b/src/wallet/test/coinselector_tests.cpp index 2abdc8a2b2e73..1f92d20b3fb06 100644 --- a/src/wallet/test/coinselector_tests.cpp +++ b/src/wallet/test/coinselector_tests.cpp @@ -731,12 +731,25 @@ BOOST_AUTO_TEST_CASE(waste_test) BOOST_CHECK_LT(waste_nochange2, waste_nochange1); selection.clear(); - // 0 Waste only when fee == long term fee, no change, and no excess + // No Waste when fee == long_term_fee, no change, and no excess add_coin(1 * COIN, 1, selection, fee, fee); add_coin(2 * COIN, 2, selection, fee, fee); - const CAmount exact_target = in_amt - 2 * fee; - BOOST_CHECK_EQUAL(0, GetSelectionWaste(selection, 0, exact_target)); + const CAmount exact_target{in_amt - fee * 2}; + BOOST_CHECK_EQUAL(0, GetSelectionWaste(selection, /* change_cost */ 0, exact_target)); + selection.clear(); + // No Waste when (fee - long_term_fee) == (-cost_of_change), and no excess + const CAmount new_change_cost{fee_diff * 2}; + add_coin(1 * COIN, 1, selection, fee, fee + fee_diff); + add_coin(2 * COIN, 2, selection, fee, fee + fee_diff); + BOOST_CHECK_EQUAL(0, GetSelectionWaste(selection, new_change_cost, target)); + selection.clear(); + + // No Waste when (fee - long_term_fee) == (-excess), no change cost + const CAmount new_target{in_amt - fee * 2 - fee_diff * 2}; + add_coin(1 * COIN, 1, selection, fee, fee + fee_diff); + add_coin(2 * COIN, 2, selection, fee, fee + fee_diff); + BOOST_CHECK_EQUAL(0, GetSelectionWaste(selection, /* change cost */ 0, new_target)); } BOOST_AUTO_TEST_SUITE_END() From f539c25fec7db746300529de6ffa3fbc02c5cd2f Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Tue, 7 Jan 2025 08:54:08 +0000 Subject: [PATCH 15/16] merge bitcoin#23288: remove usage of LegacyScriptPubKeyMan from some wallet tests Dash-specific tests have not been migrated to use descriptor wallets and the old AddKey() logic has been retained as AddLegacyKey(). final_hex needed to be recalculated in psbt_updater_test as one of the input scripts were an invalid-to-Dash address and its replacement with a descriptor that doesn't correspond to the same script requires recalculating the unsigned PSBT. excludes: - 99516285 (we don't support bech32 addresses) --- src/bench/coin_selection.cpp | 3 +- src/bench/wallet_balance.cpp | 16 +++---- src/qt/test/addressbooktests.cpp | 6 ++- src/qt/test/wallettests.cpp | 17 ++++++-- src/test/util/wallet.cpp | 13 ------ src/wallet/test/coinselector_tests.cpp | 31 +++++++------ src/wallet/test/psbt_wallet_tests.cpp | 39 +++++++++-------- src/wallet/test/util.cpp | 14 ++++-- src/wallet/test/wallet_tests.cpp | 60 +++++++++++++++++++------- 9 files changed, 118 insertions(+), 81 deletions(-) diff --git a/src/bench/coin_selection.cpp b/src/bench/coin_selection.cpp index 6fd1ed6108ed1..030b906294cf6 100644 --- a/src/bench/coin_selection.cpp +++ b/src/bench/coin_selection.cpp @@ -32,7 +32,6 @@ static void CoinSelection(benchmark::Bench& bench) NodeContext node; auto chain = interfaces::MakeChain(node); CWallet wallet(chain.get(), /*coinjoin_loader=*/ nullptr, "", CreateDummyWalletDatabase()); - wallet.SetupLegacyScriptPubKeyMan(); std::vector> wtxs; LOCK(wallet.cs_wallet); @@ -51,7 +50,7 @@ static void CoinSelection(benchmark::Bench& bench) const CoinSelectionParams coin_selection_params(/* change_output_size= */ 34, /* change_spend_size= */ 148, /* effective_feerate= */ CFeeRate(0), /* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0), - /* tx_no_inputs_size= */ 0, /* avoid_partial= */ false); + /* tx_noinputs_size= */ 0, /* avoid_partial= */ false); bench.run([&] { std::set setCoinsRet; CAmount nValueRet; diff --git a/src/bench/wallet_balance.cpp b/src/bench/wallet_balance.cpp index cf70367a30f1b..75ffb955e4651 100644 --- a/src/bench/wallet_balance.cpp +++ b/src/bench/wallet_balance.cpp @@ -13,20 +13,21 @@ #include -static void WalletBalance(benchmark::Bench& bench, const bool set_dirty, const bool add_watchonly, const bool add_mine, const uint32_t epoch_iters) +static void WalletBalance(benchmark::Bench& bench, const bool set_dirty, const bool add_mine, const uint32_t epoch_iters) { const auto test_setup = MakeNoLogFileContext(); const auto& ADDRESS_WATCHONLY = ADDRESS_B58T_UNSPENDABLE; CWallet wallet{test_setup->m_node.chain.get(), test_setup->m_node.coinjoin_loader.get(), "", CreateMockWalletDatabase()}; { - wallet.SetupLegacyScriptPubKeyMan(); + LOCK(wallet.cs_wallet); + wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); + wallet.SetupDescriptorScriptPubKeyMans(); if (wallet.LoadWallet() != DBErrors::LOAD_OK) assert(false); } auto handler = test_setup->m_node.chain->handleNotifications({&wallet, [](CWallet*) {}}); const std::optional address_mine{add_mine ? std::optional{getnewaddress(wallet)} : std::nullopt}; - if (add_watchonly) importaddress(wallet, ADDRESS_WATCHONLY); for (int i = 0; i < 100; ++i) { generatetoaddress(test_setup->m_node, address_mine.value_or(ADDRESS_WATCHONLY)); @@ -40,14 +41,13 @@ static void WalletBalance(benchmark::Bench& bench, const bool set_dirty, const b if (set_dirty) wallet.MarkDirty(); bal = wallet.GetBalance(); if (add_mine) assert(bal.m_mine_trusted > 0); - if (add_watchonly) assert(bal.m_watchonly_trusted > 0); }); } -static void WalletBalanceDirty(benchmark::Bench& bench) { WalletBalance(bench, /* set_dirty */ true, /* add_watchonly */ true, /* add_mine */ true, 2500); } -static void WalletBalanceClean(benchmark::Bench& bench) {WalletBalance(bench, /* set_dirty */ false, /* add_watchonly */ true, /* add_mine */ true, 8000); } -static void WalletBalanceMine(benchmark::Bench& bench) { WalletBalance(bench, /* set_dirty */ false, /* add_watchonly */ false, /* add_mine */ true, 16000); } -static void WalletBalanceWatch(benchmark::Bench& bench) { WalletBalance(bench, /* set_dirty */ false, /* add_watchonly */ true, /* add_mine */ false, 8000); } +static void WalletBalanceDirty(benchmark::Bench& bench) { WalletBalance(bench, /* set_dirty */ true, /* add_mine */ true, 2500); } +static void WalletBalanceClean(benchmark::Bench& bench) {WalletBalance(bench, /* set_dirty */ false, /* add_mine */ true, 8000); } +static void WalletBalanceMine(benchmark::Bench& bench) { WalletBalance(bench, /* set_dirty */ false, /* add_mine */ true, 16000); } +static void WalletBalanceWatch(benchmark::Bench& bench) { WalletBalance(bench, /* set_dirty */ false, /* add_mine */ false, 8000); } BENCHMARK(WalletBalanceDirty); BENCHMARK(WalletBalanceClean); diff --git a/src/qt/test/addressbooktests.cpp b/src/qt/test/addressbooktests.cpp index e9f4ff46fbac4..ac0a8357a8b52 100644 --- a/src/qt/test/addressbooktests.cpp +++ b/src/qt/test/addressbooktests.cpp @@ -63,8 +63,12 @@ void TestAddAddressesToSendBook(interfaces::Node& node) TestChain100Setup test; node.setContext(&test.m_node); std::shared_ptr wallet = std::make_shared(node.context()->chain.get(), node.context()->coinjoin_loader.get(), "", CreateMockWalletDatabase()); - wallet->SetupLegacyScriptPubKeyMan(); wallet->LoadWallet(); + wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS); + { + LOCK(wallet->cs_wallet); + wallet->SetupDescriptorScriptPubKeyMans(); + } auto build_address = [wallet]() { CKey key; diff --git a/src/qt/test/wallettests.cpp b/src/qt/test/wallettests.cpp index e71293ea9a84e..f1a525e606673 100644 --- a/src/qt/test/wallettests.cpp +++ b/src/qt/test/wallettests.cpp @@ -113,11 +113,20 @@ void TestGUI(interfaces::Node& node) std::shared_ptr wallet = std::make_shared(node.context()->chain.get(), node.context()->coinjoin_loader.get(), "", CreateMockWalletDatabase()); AddWallet(context, wallet); wallet->LoadWallet(); + wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS); { - auto spk_man = wallet->GetOrCreateLegacyScriptPubKeyMan(); - LOCK2(wallet->cs_wallet, spk_man->cs_KeyStore); - wallet->SetAddressBook(PKHash(test.coinbaseKey.GetPubKey()), "", "receive"); - spk_man->AddKeyPubKey(test.coinbaseKey, test.coinbaseKey.GetPubKey()); + LOCK(wallet->cs_wallet); + wallet->SetupDescriptorScriptPubKeyMans(); + + // Add the coinbase key + FlatSigningProvider provider; + std::string error; + std::unique_ptr desc = Parse("combo(" + EncodeSecret(test.coinbaseKey) + ")", provider, error, /* require_checksum=*/ false); + assert(desc); + WalletDescriptor w_desc(std::move(desc), 0, 0, 1, 1); + if (!wallet->AddWalletDescriptor(w_desc, provider, "", false)) assert(false); + CTxDestination dest = PKHash(test.coinbaseKey.GetPubKey()); + wallet->SetAddressBook(dest, "", "receive"); wallet->SetLastBlockProcessed(105, node.context()->chainman->ActiveChain().Tip()->GetBlockHash()); } { diff --git a/src/test/util/wallet.cpp b/src/test/util/wallet.cpp index ee0457097db52..ef5e4a26490d9 100644 --- a/src/test/util/wallet.cpp +++ b/src/test/util/wallet.cpp @@ -25,17 +25,4 @@ std::string getnewaddress(CWallet& w) return EncodeDestination(dest); } -void importaddress(CWallet& wallet, const std::string& address) -{ - auto spk_man = wallet.GetLegacyScriptPubKeyMan(); - assert(spk_man != nullptr); - LOCK2(wallet.cs_wallet, spk_man->cs_KeyStore); - const auto dest = DecodeDestination(address); - assert(IsValidDestination(dest)); - const auto script = GetScriptForDestination(dest); - wallet.MarkDirty(); - assert(!spk_man->HaveWatchOnly(script)); - if (!spk_man->AddWatchOnly(script, 0 /* nCreateTime */)) assert(false); - wallet.SetAddressBook(dest, /* label */ "", "receive"); -} #endif // ENABLE_WALLET diff --git a/src/wallet/test/coinselector_tests.cpp b/src/wallet/test/coinselector_tests.cpp index 1f92d20b3fb06..1f848051a861a 100644 --- a/src/wallet/test/coinselector_tests.cpp +++ b/src/wallet/test/coinselector_tests.cpp @@ -29,16 +29,10 @@ BOOST_FIXTURE_TEST_SUITE(coinselector_tests, WalletTestingSetup) typedef std::set CoinSet; -/* TODO: Revert current globals removal and backport pull requests implementing it */ static const CoinEligibilityFilter filter_standard(1, 6, 0); static const CoinEligibilityFilter filter_confirmed(1, 1, 0); static const CoinEligibilityFilter filter_standard_extra(6, 6, 0); -CoinSelectionParams coin_selection_params(/* change_output_size= */ 0, - /* change_spend_size= */ 0, /* effective_feerate= */ CFeeRate(0), - /* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0), - /* tx_no_inputs_size= */ 0, /* avoid_partial= */ false); - static void add_coin(const CAmount& nValue, int nInput, std::vector& set) { CMutableTransaction tx; @@ -278,12 +272,13 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) CoinSelectionParams coin_selection_params_bnb(/* change_output_size= */ 0, /* change_spend_size= */ 0, /* effective_feerate= */ CFeeRate(3000), /* long_term_feerate= */ CFeeRate(1000), /* discard_feerate= */ CFeeRate(1000), - /* tx_no_inputs_size= */ 0, /* avoid_partial= */ false); + /* tx_noinputs_size= */ 0, /* avoid_partial= */ false); { std::unique_ptr wallet = std::make_unique(m_node.chain.get(), /* coinjoin_loader = */ nullptr, "", CreateMockWalletDatabase()); - wallet->SetupLegacyScriptPubKeyMan(); wallet->LoadWallet(); LOCK(wallet->cs_wallet); + wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS); + wallet->SetupDescriptorScriptPubKeyMans(); std::vector coins; CoinSet setCoinsRet; @@ -305,8 +300,9 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) { std::unique_ptr wallet = std::make_unique(m_node.chain.get(), /* coinjoin_loader = */ nullptr, "", CreateMockWalletDatabase()); wallet->LoadWallet(); - wallet->SetupLegacyScriptPubKeyMan(); LOCK(wallet->cs_wallet); + wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS); + wallet->SetupDescriptorScriptPubKeyMans(); std::vector coins; CoinSet setCoinsRet; @@ -327,8 +323,9 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) { std::unique_ptr wallet = std::make_unique(m_node.chain.get(), /* coinjoin_loader = */ nullptr, "", CreateMockWalletDatabase()); wallet->LoadWallet(); - wallet->SetupLegacyScriptPubKeyMan(); LOCK(wallet->cs_wallet); + wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS); + wallet->SetupDescriptorScriptPubKeyMans(); CoinSet setCoinsRet, setCoinsRet2; CAmount nValueRet; @@ -533,9 +530,9 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) for (uint16_t j = 0; j < 676; j++) add_coin(coins, *wallet, amt); - // We only create the wallet once to save time, but we still run the coin selection RUN_TESTS times. - for (int i = 0; i < RUN_TESTS; i++) { - BOOST_CHECK(KnapsackSolver(2000, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), setCoinsRet, nValueRet)); + // We only create the wallet once to save time, but we still run the coin selection RUN_TESTS times. + for (int i = 0; i < RUN_TESTS; i++) { + BOOST_CHECK(KnapsackSolver(2000, KnapsackGroupOutputs(coins, *wallet, filter_confirmed), setCoinsRet, nValueRet)); if (amt - 2000 < MIN_CHANGE) { // needs more than one input: @@ -607,8 +604,9 @@ BOOST_AUTO_TEST_CASE(ApproximateBestSubset) { std::unique_ptr wallet = std::make_unique(m_node.chain.get(), /* coinjoin_loader = */ nullptr, "", CreateMockWalletDatabase()); wallet->LoadWallet(); - wallet->SetupLegacyScriptPubKeyMan(); LOCK(wallet->cs_wallet); + wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS); + wallet->SetupDescriptorScriptPubKeyMans(); CoinSet setCoinsRet; CAmount nValueRet; @@ -629,8 +627,9 @@ BOOST_AUTO_TEST_CASE(SelectCoins_test) { std::unique_ptr wallet = std::make_unique(m_node.chain.get(), /* coinjoin_loader = */ nullptr, "", CreateMockWalletDatabase()); wallet->LoadWallet(); - wallet->SetupLegacyScriptPubKeyMan(); LOCK(wallet->cs_wallet); + wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS); + wallet->SetupDescriptorScriptPubKeyMans(); // Random generator stuff std::default_random_engine generator; @@ -661,7 +660,7 @@ BOOST_AUTO_TEST_CASE(SelectCoins_test) CoinSelectionParams cs_params(/* change_output_size= */ 34, /* change_spend_size= */ 148, /* effective_feerate= */ CFeeRate(0), /* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0), - /* tx_no_inputs_size= */ 0, /* avoid_partial= */ false); + /* tx_noinputs_size= */ 0, /* avoid_partial= */ false); CoinSet out_set; CAmount out_value = 0; CCoinControl cc; diff --git a/src/wallet/test/psbt_wallet_tests.cpp b/src/wallet/test/psbt_wallet_tests.cpp index b61c666484c0f..bc858416ff49f 100644 --- a/src/wallet/test/psbt_wallet_tests.cpp +++ b/src/wallet/test/psbt_wallet_tests.cpp @@ -14,10 +14,21 @@ extern bool ParseHDKeypath(const std::string& keypath_str, std::vector BOOST_FIXTURE_TEST_SUITE(psbt_wallet_tests, WalletTestingSetup) +static void import_descriptor(CWallet& wallet, const std::string& descriptor) +{ + LOCK(wallet.cs_wallet); + FlatSigningProvider provider; + std::string error; + std::unique_ptr desc = Parse(descriptor, provider, error, /* require_checksum=*/ false); + assert(desc); + WalletDescriptor w_desc(std::move(desc), 0, 0, 10, 0); + wallet.AddWalletDescriptor(w_desc, provider, "", false); +} + BOOST_AUTO_TEST_CASE(psbt_updater_test) { - auto spk_man = m_wallet.GetOrCreateLegacyScriptPubKeyMan(); - LOCK2(m_wallet.cs_wallet, spk_man->cs_KeyStore); + LOCK(m_wallet.cs_wallet); + m_wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); // Create prevtxs and add to wallet CDataStream s_prev_tx1(ParseHex("0200000000010158e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7501000000171600145f275f436b09a8cc9a2eb2a2f528485c68a56323feffffff02d8231f1b0100000017a914aed962d6654f9a2b36608eb9d64d2b260db4f1118700c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e88702483045022100a22edcc6e5bc511af4cc4ae0de0fcd75c7e04d8c1c3a8aa9d820ed4b967384ec02200642963597b9b1bc22c75e9f3e117284a962188bf5e8a74c895089046a20ad770121035509a48eb623e10aace8bfd0212fdb8a8e5af3c94b0b133b95e114cab89e4f7965000000"), SER_NETWORK, PROTOCOL_VERSION); @@ -30,21 +41,10 @@ BOOST_AUTO_TEST_CASE(psbt_updater_test) s_prev_tx2 >> prev_tx2; m_wallet.mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(prev_tx2->GetHash()), std::forward_as_tuple(&m_wallet, prev_tx2)); - // Add scripts - CScript rs1; - CDataStream s_rs1(ParseHex("475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae"), SER_NETWORK, PROTOCOL_VERSION); - s_rs1 >> rs1; - spk_man->AddCScript(rs1); - - CScript rs2; - CDataStream s_rs2(ParseHex("2200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903"), SER_NETWORK, PROTOCOL_VERSION); - s_rs2 >> rs2; - spk_man->AddCScript(rs2); - - CScript ws1; - CDataStream s_ws1(ParseHex("47522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae"), SER_NETWORK, PROTOCOL_VERSION); - s_ws1 >> ws1; - spk_man->AddCScript(ws1); + // Import descriptors for keys and scripts + import_descriptor(m_wallet, "sh(multi(2,xprv9s21ZrQH143K2LE7W4Xf3jATf9jECxSb7wj91ZnmY4qEJrS66Qru9RFqq8xbkgT32ya6HqYJweFdJUEDf5Q6JFV7jMiUws7kQfe6Tv4RbfN/0h/0h/0h,xprv9s21ZrQH143K2LE7W4Xf3jATf9jECxSb7wj91ZnmY4qEJrS66Qru9RFqq8xbkgT32ya6HqYJweFdJUEDf5Q6JFV7jMiUws7kQfe6Tv4RbfN/0h/0h/1h))"); + import_descriptor(m_wallet, "sh(multi(2,xprv9s21ZrQH143K2LE7W4Xf3jATf9jECxSb7wj91ZnmY4qEJrS66Qru9RFqq8xbkgT32ya6HqYJweFdJUEDf5Q6JFV7jMiUws7kQfe6Tv4RbfN/0h/0h/2h,xprv9s21ZrQH143K2LE7W4Xf3jATf9jECxSb7wj91ZnmY4qEJrS66Qru9RFqq8xbkgT32ya6HqYJweFdJUEDf5Q6JFV7jMiUws7kQfe6Tv4RbfN/0h/0h/3h))"); + import_descriptor(m_wallet, "pkh(xprv9s21ZrQH143K2LE7W4Xf3jATf9jECxSb7wj91ZnmY4qEJrS66Qru9RFqq8xbkgT32ya6HqYJweFdJUEDf5Q6JFV7jMiUws7kQfe6Tv4RbfN/0h/0h/*h)"); // Call FillPSBT PartiallySignedTransaction psbtx; @@ -64,14 +64,15 @@ BOOST_AUTO_TEST_CASE(psbt_updater_test) ssTx << psbtx; std::string final_hex = HexStr(ssTx); - BOOST_CHECK_EQUAL(final_hex, "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f049c4942a9220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d704b9147fd300000000"); + BOOST_CHECK_EQUAL(final_hex, "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f00000080000000800100008000000000"); // Mutate the transaction so that one of the inputs is invalid psbtx.tx->vin[0].prevout.n = 2; // Try to sign the mutated input SignatureData sigdata; - BOOST_CHECK(spk_man->FillPSBT(psbtx, PrecomputePSBTData(psbtx), SIGHASH_ALL, true, true) != TransactionError::OK); + BOOST_CHECK(m_wallet.FillPSBT(psbtx, complete, SIGHASH_ALL, true, true) != TransactionError::OK); + // BOOST_CHECK(spk_man->FillPSBT(psbtx, PrecomputePSBTData(psbtx), SIGHASH_ALL, true, true) != TransactionError::OK); } BOOST_AUTO_TEST_CASE(parse_hd_keypath) diff --git a/src/wallet/test/util.cpp b/src/wallet/test/util.cpp index e8015dfadb8aa..a020e4b04f763 100644 --- a/src/wallet/test/util.cpp +++ b/src/wallet/test/util.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -23,9 +24,16 @@ std::unique_ptr CreateSyncedWallet(interfaces::Chain& chain, interfaces } wallet->LoadWallet(); { - auto spk_man = wallet->GetOrCreateLegacyScriptPubKeyMan(); - LOCK2(wallet->cs_wallet, spk_man->cs_KeyStore); - spk_man->AddKeyPubKey(key, key.GetPubKey()); + LOCK(wallet->cs_wallet); + wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS); + wallet->SetupDescriptorScriptPubKeyMans(); + + FlatSigningProvider provider; + std::string error; + std::unique_ptr desc = Parse("combo(" + EncodeSecret(key) + ")", provider, error, /* require_checksum=*/ false); + assert(desc); + WalletDescriptor w_desc(std::move(desc), 0, 0, 1, 1); + if (!wallet->AddWalletDescriptor(w_desc, provider, "", false)) assert(false); } WalletRescanReserver reserver(*wallet); reserver.reserve(); diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index 0eecc71b039b6..f87e44adbda86 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -51,6 +51,7 @@ BOOST_FIXTURE_TEST_SUITE(wallet_tests, WalletTestingSetup) static std::shared_ptr TestLoadWallet(WalletContext& context) { DatabaseOptions options; + options.create_flags = WALLET_FLAG_DESCRIPTORS; DatabaseStatus status; bilingual_str error; std::vector warnings; @@ -91,9 +92,13 @@ static CMutableTransaction TestSimpleSpend(const CTransaction& from, uint32_t in static void AddKey(CWallet& wallet, const CKey& key) { - auto spk_man = wallet.GetOrCreateLegacyScriptPubKeyMan(); - LOCK2(wallet.cs_wallet, spk_man->cs_KeyStore); - spk_man->AddKeyPubKey(key, key.GetPubKey()); + LOCK(wallet.cs_wallet); + FlatSigningProvider provider; + std::string error; + std::unique_ptr desc = Parse("combo(" + EncodeSecret(key) + ")", provider, error, /* require_checksum=*/ false); + assert(desc); + WalletDescriptor w_desc(std::move(desc), 0, 0, 1, 1); + if (!wallet.AddWalletDescriptor(w_desc, provider, "", false)) assert(false); } BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup) @@ -110,6 +115,7 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup) wallet.SetupLegacyScriptPubKeyMan(); { LOCK(wallet.cs_wallet); + wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); } AddKey(wallet, coinbaseKey); @@ -129,6 +135,7 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup) CWallet wallet(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateDummyWalletDatabase()); { LOCK(wallet.cs_wallet); + wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); } AddKey(wallet, coinbaseKey); @@ -157,6 +164,7 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup) CWallet wallet(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateDummyWalletDatabase()); { LOCK(wallet.cs_wallet); + wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); } AddKey(wallet, coinbaseKey); @@ -183,6 +191,7 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup) CWallet wallet(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateDummyWalletDatabase()); { LOCK(wallet.cs_wallet); + wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); } AddKey(wallet, coinbaseKey); @@ -340,10 +349,12 @@ BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup) BOOST_FIXTURE_TEST_CASE(coin_mark_dirty_immature_credit, TestChain100Setup) { CWallet wallet(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateDummyWalletDatabase()); - auto spk_man = wallet.GetOrCreateLegacyScriptPubKeyMan(); CWalletTx wtx(&wallet, m_coinbase_txns.back()); - LOCK2(wallet.cs_wallet, spk_man->cs_KeyStore); + LOCK(wallet.cs_wallet); + wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); + wallet.SetupDescriptorScriptPubKeyMans(); + wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); CWalletTx::Confirmation confirm(CWalletTx::Status::CONFIRMED, m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash(), 0); @@ -356,7 +367,7 @@ BOOST_FIXTURE_TEST_CASE(coin_mark_dirty_immature_credit, TestChain100Setup) // Invalidate the cached value, add the key, and make sure a new immature // credit amount is calculated. wtx.MarkDirty(); - BOOST_CHECK(spk_man->AddKeyPubKey(coinbaseKey, coinbaseKey.GetPubKey())); + AddKey(wallet, coinbaseKey); BOOST_CHECK_EQUAL(wtx.GetImmatureCredit(), 500*COIN); } @@ -613,14 +624,26 @@ BOOST_FIXTURE_TEST_CASE(ListCoins, ListCoinsTestingSetup) BOOST_FIXTURE_TEST_CASE(wallet_disableprivkeys, TestChain100Setup) { - std::shared_ptr wallet = std::make_shared(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateDummyWalletDatabase()); - wallet->SetupLegacyScriptPubKeyMan(); - wallet->SetMinVersion(FEATURE_LATEST); - wallet->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS); - BOOST_CHECK(!wallet->TopUpKeyPool(1000)); - CTxDestination dest; - bilingual_str error; - BOOST_CHECK(!wallet->GetNewDestination("", dest, error)); + { + std::shared_ptr wallet = std::make_shared(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateDummyWalletDatabase()); + wallet->SetupLegacyScriptPubKeyMan(); + wallet->SetMinVersion(FEATURE_LATEST); + wallet->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS); + BOOST_CHECK(!wallet->TopUpKeyPool(1000)); + CTxDestination dest; + bilingual_str error; + BOOST_CHECK(!wallet->GetNewDestination("", dest, error)); + } + { + std::shared_ptr wallet = std::make_shared(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateDummyWalletDatabase()); + LOCK(wallet->cs_wallet); + wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS); + wallet->SetMinVersion(FEATURE_LATEST); + wallet->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS); + CTxDestination dest; + bilingual_str error; + BOOST_CHECK(!wallet->GetNewDestination("", dest, error)); + } } // Explicit calculation which is used to test the wallet constant @@ -847,6 +870,13 @@ namespace { constexpr CAmount fallbackFee = 1000; } // anonymous namespace +static void AddLegacyKey(CWallet& wallet, const CKey& key) +{ + auto spk_man = wallet.GetOrCreateLegacyScriptPubKeyMan(); + LOCK2(wallet.cs_wallet, spk_man->cs_KeyStore); + spk_man->AddKeyPubKey(key, key.GetPubKey()); +} + // Verify getaddressinfo RPC produces more or less expected results BOOST_FIXTURE_TEST_CASE(rpc_getaddressinfo, TestChain100Setup) { @@ -958,7 +988,7 @@ class CreateTransactionTestSetup : public TestChain100Setup wallet = std::make_unique(m_node.chain.get(), m_node.coinjoin_loader.get(), "", CreateMockWalletDatabase()); wallet->LoadWallet(); AddWallet(context, wallet); - AddKey(*wallet, coinbaseKey); + AddLegacyKey(*wallet, coinbaseKey); WalletRescanReserver reserver(*wallet); reserver.reserve(); { From 196f1e3dd1b45526a0e5b8e2bd382814e8098dea Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Wed, 16 Mar 2022 19:30:46 +0000 Subject: [PATCH 16/16] merge bitcoin#24592: Delete old line of code that was commented out --- src/wallet/test/psbt_wallet_tests.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wallet/test/psbt_wallet_tests.cpp b/src/wallet/test/psbt_wallet_tests.cpp index bc858416ff49f..d6abff3a759d5 100644 --- a/src/wallet/test/psbt_wallet_tests.cpp +++ b/src/wallet/test/psbt_wallet_tests.cpp @@ -72,7 +72,6 @@ BOOST_AUTO_TEST_CASE(psbt_updater_test) // Try to sign the mutated input SignatureData sigdata; BOOST_CHECK(m_wallet.FillPSBT(psbtx, complete, SIGHASH_ALL, true, true) != TransactionError::OK); - // BOOST_CHECK(spk_man->FillPSBT(psbtx, PrecomputePSBTData(psbtx), SIGHASH_ALL, true, true) != TransactionError::OK); } BOOST_AUTO_TEST_CASE(parse_hd_keypath)