aboutsummaryrefslogtreecommitdiff
path: root/src/wallet/test
diff options
context:
space:
mode:
Diffstat (limited to 'src/wallet/test')
-rw-r--r--src/wallet/test/coinselector_tests.cpp211
-rw-r--r--src/wallet/test/db_tests.cpp35
-rw-r--r--src/wallet/test/fuzz/coinselection.cpp144
-rw-r--r--src/wallet/test/fuzz/notifications.cpp6
-rw-r--r--src/wallet/test/spend_tests.cpp6
-rw-r--r--src/wallet/test/util.cpp3
-rw-r--r--src/wallet/test/util.h2
-rw-r--r--src/wallet/test/wallet_tests.cpp22
-rw-r--r--src/wallet/test/walletload_tests.cpp1
9 files changed, 410 insertions, 20 deletions
diff --git a/src/wallet/test/coinselector_tests.cpp b/src/wallet/test/coinselector_tests.cpp
index 9fea14145f..9a349f0992 100644
--- a/src/wallet/test/coinselector_tests.cpp
+++ b/src/wallet/test/coinselector_tests.cpp
@@ -1090,6 +1090,216 @@ BOOST_AUTO_TEST_CASE(effective_value_test)
BOOST_CHECK_EQUAL(output5.GetEffectiveValue(), nValue); // The effective value should be equal to the absolute value if input_bytes is -1
}
+static util::Result<SelectionResult> CoinGrinder(const CAmount& target,
+ const CoinSelectionParams& cs_params,
+ const node::NodeContext& m_node,
+ int max_weight,
+ std::function<CoinsResult(CWallet&)> coin_setup)
+{
+ std::unique_ptr<CWallet> wallet = NewWallet(m_node);
+ CoinEligibilityFilter filter(0, 0, 0); // accept all coins without ancestors
+ Groups group = GroupOutputs(*wallet, coin_setup(*wallet), cs_params, {{filter}})[filter].all_groups;
+ return CoinGrinder(group.positive_group, target, cs_params.m_min_change_target, max_weight);
+}
+
+BOOST_AUTO_TEST_CASE(coin_grinder_tests)
+{
+ // Test Coin Grinder:
+ // 1) Insufficient funds, select all provided coins and fail.
+ // 2) Exceeded max weight, coin selection always surpasses the max allowed weight.
+ // 3) Select coins without surpassing the max weight (some coins surpasses the max allowed weight, some others not)
+ // 4) Test that two less valuable UTXOs with a combined lower weight are preferred over a more valuable heavier UTXO
+ // 5) Test finding a solution in a UTXO pool with mixed weights
+ // 6) Test that the lightest solution among many clones is found
+ // 7) Test that lots of tiny UTXOs can be skipped if they are too heavy while there are enough funds in lookahead
+
+ FastRandomContext rand;
+ CoinSelectionParams dummy_params{ // Only used to provide the 'avoid_partial' flag.
+ rand,
+ /*change_output_size=*/34,
+ /*change_spend_size=*/68,
+ /*min_change_target=*/CENT,
+ /*effective_feerate=*/CFeeRate(5000),
+ /*long_term_feerate=*/CFeeRate(2000),
+ /*discard_feerate=*/CFeeRate(1000),
+ /*tx_noinputs_size=*/10 + 34, // static header size + output size
+ /*avoid_partial=*/false,
+ };
+
+ {
+ // #########################################################
+ // 1) Insufficient funds, select all provided coins and fail
+ // #########################################################
+ CAmount target = 49.5L * COIN;
+ int max_weight = 10'000; // high enough to not fail for this reason.
+ const auto& res = CoinGrinder(target, dummy_params, m_node, max_weight, [&](CWallet& wallet) {
+ CoinsResult available_coins;
+ for (int j = 0; j < 10; ++j) {
+ add_coin(available_coins, wallet, CAmount(1 * COIN));
+ add_coin(available_coins, wallet, CAmount(2 * COIN));
+ }
+ return available_coins;
+ });
+ BOOST_CHECK(!res);
+ BOOST_CHECK(util::ErrorString(res).empty()); // empty means "insufficient funds"
+ }
+
+ {
+ // ###########################
+ // 2) Test max weight exceeded
+ // ###########################
+ CAmount target = 29.5L * COIN;
+ int max_weight = 3000;
+ const auto& res = CoinGrinder(target, dummy_params, m_node, max_weight, [&](CWallet& wallet) {
+ CoinsResult available_coins;
+ for (int j = 0; j < 10; ++j) {
+ add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true);
+ add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(5000), 144, false, 0, true);
+ }
+ return available_coins;
+ });
+ BOOST_CHECK(!res);
+ BOOST_CHECK(util::ErrorString(res).original.find("The inputs size exceeds the maximum weight") != std::string::npos);
+ }
+
+ {
+ // ###############################################################################################################
+ // 3) Test selection when some coins surpass the max allowed weight while others not. --> must find a good solution
+ // ################################################################################################################
+ CAmount target = 25.33L * COIN;
+ int max_weight = 10'000; // WU
+ const auto& res = CoinGrinder(target, dummy_params, m_node, max_weight, [&](CWallet& wallet) {
+ CoinsResult available_coins;
+ for (int j = 0; j < 60; ++j) { // 60 UTXO --> 19,8 BTC total --> 60 × 272 WU = 16320 WU
+ add_coin(available_coins, wallet, CAmount(0.33 * COIN), CFeeRate(5000), 144, false, 0, true);
+ }
+ for (int i = 0; i < 10; i++) { // 10 UTXO --> 20 BTC total --> 10 × 272 WU = 2720 WU
+ add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(5000), 144, false, 0, true);
+ }
+ return available_coins;
+ });
+ BOOST_CHECK(res);
+ // Demonstrate how following improvements reduce iteration count and catch any regressions in the future.
+ size_t expected_attempts = 37;
+ BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
+ }
+
+ {
+ // #################################################################################################################
+ // 4) Test that two less valuable UTXOs with a combined lower weight are preferred over a more valuable heavier UTXO
+ // #################################################################################################################
+ CAmount target = 1.9L * COIN;
+ int max_weight = 400'000; // WU
+ const auto& res = CoinGrinder(target, dummy_params, m_node, max_weight, [&](CWallet& wallet) {
+ CoinsResult available_coins;
+ add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(5000), 144, false, 0, true, 148);
+ add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 68);
+ add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 68);
+ return available_coins;
+ });
+ SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG);
+ add_coin(1 * COIN, 1, expected_result);
+ add_coin(1 * COIN, 2, expected_result);
+ BOOST_CHECK(EquivalentResult(expected_result, *res));
+ // Demonstrate how following improvements reduce iteration count and catch any regressions in the future.
+ size_t expected_attempts = 3;
+ BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
+ }
+
+ {
+ // ###############################################################################################################
+ // 5) Test finding a solution in a UTXO pool with mixed weights
+ // ################################################################################################################
+ CAmount target = 30L * COIN;
+ int max_weight = 400'000; // WU
+ const auto& res = CoinGrinder(target, dummy_params, m_node, max_weight, [&](CWallet& wallet) {
+ CoinsResult available_coins;
+ for (int j = 0; j < 5; ++j) {
+ // Add heavy coins {3, 6, 9, 12, 15}
+ add_coin(available_coins, wallet, CAmount((3 + 3 * j) * COIN), CFeeRate(5000), 144, false, 0, true, 350);
+ // Add medium coins {2, 5, 8, 11, 14}
+ add_coin(available_coins, wallet, CAmount((2 + 3 * j) * COIN), CFeeRate(5000), 144, false, 0, true, 250);
+ // Add light coins {1, 4, 7, 10, 13}
+ add_coin(available_coins, wallet, CAmount((1 + 3 * j) * COIN), CFeeRate(5000), 144, false, 0, true, 150);
+ }
+ return available_coins;
+ });
+ BOOST_CHECK(res);
+ SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG);
+ add_coin(14 * COIN, 1, expected_result);
+ add_coin(13 * COIN, 2, expected_result);
+ add_coin(4 * COIN, 3, expected_result);
+ BOOST_CHECK(EquivalentResult(expected_result, *res));
+ // Demonstrate how following improvements reduce iteration count and catch any regressions in the future.
+ size_t expected_attempts = 92;
+ BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
+ }
+
+ {
+ // #################################################################################################################
+ // 6) Test that the lightest solution among many clones is found
+ // #################################################################################################################
+ CAmount target = 9.9L * COIN;
+ int max_weight = 400'000; // WU
+ const auto& res = CoinGrinder(target, dummy_params, m_node, max_weight, [&](CWallet& wallet) {
+ CoinsResult available_coins;
+ // Expected Result: 4 + 3 + 2 + 1 = 10 BTC at 400 vB
+ add_coin(available_coins, wallet, CAmount(4 * COIN), CFeeRate(5000), 144, false, 0, true, 100);
+ add_coin(available_coins, wallet, CAmount(3 * COIN), CFeeRate(5000), 144, false, 0, true, 100);
+ add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(5000), 144, false, 0, true, 100);
+ add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 100);
+ // Distracting clones:
+ for (int j = 0; j < 100; ++j) {
+ add_coin(available_coins, wallet, CAmount(8 * COIN), CFeeRate(5000), 144, false, 0, true, 1000);
+ }
+ for (int j = 0; j < 100; ++j) {
+ add_coin(available_coins, wallet, CAmount(7 * COIN), CFeeRate(5000), 144, false, 0, true, 800);
+ }
+ for (int j = 0; j < 100; ++j) {
+ add_coin(available_coins, wallet, CAmount(6 * COIN), CFeeRate(5000), 144, false, 0, true, 600);
+ }
+ for (int j = 0; j < 100; ++j) {
+ add_coin(available_coins, wallet, CAmount(5 * COIN), CFeeRate(5000), 144, false, 0, true, 400);
+ }
+ return available_coins;
+ });
+ SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG);
+ add_coin(4 * COIN, 0, expected_result);
+ add_coin(3 * COIN, 0, expected_result);
+ add_coin(2 * COIN, 0, expected_result);
+ add_coin(1 * COIN, 0, expected_result);
+ BOOST_CHECK(EquivalentResult(expected_result, *res));
+ // Demonstrate how following improvements reduce iteration count and catch any regressions in the future.
+ size_t expected_attempts = 38;
+ BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
+ }
+
+ {
+ // #################################################################################################################
+ // 7) Test that lots of tiny UTXOs can be skipped if they are too heavy while there are enough funds in lookahead
+ // #################################################################################################################
+ CAmount target = 1.9L * COIN;
+ int max_weight = 40000; // WU
+ const auto& res = CoinGrinder(target, dummy_params, m_node, max_weight, [&](CWallet& wallet) {
+ CoinsResult available_coins;
+ add_coin(available_coins, wallet, CAmount(1.8 * COIN), CFeeRate(5000), 144, false, 0, true, 2500);
+ add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 1000);
+ add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 1000);
+ for (int j = 0; j < 100; ++j) {
+ // make a 100 unique coins only differing by one sat
+ add_coin(available_coins, wallet, CAmount(0.01 * COIN + j), CFeeRate(5000), 144, false, 0, true, 110);
+ }
+ return available_coins;
+ });
+ SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG);
+ add_coin(1 * COIN, 1, expected_result);
+ add_coin(1 * COIN, 2, expected_result);
+ BOOST_CHECK(EquivalentResult(expected_result, *res));
+ // Demonstrate how following improvements reduce iteration count and catch any regressions in the future.
+ size_t expected_attempts = 7;
+ BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
+ }
+}
static util::Result<SelectionResult> SelectCoinsSRD(const CAmount& target,
const CoinSelectionParams& cs_params,
@@ -1150,6 +1360,7 @@ BOOST_AUTO_TEST_CASE(srd_tests)
const auto& res = SelectCoinsSRD(target, dummy_params, m_node, max_weight, [&](CWallet& wallet) {
CoinsResult available_coins;
for (int j = 0; j < 10; ++j) {
+ /* 10 × 1 BTC + 10 × 2 BTC = 30 BTC. 20 × 272 WU = 5440 WU */
add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(0), 144, false, 0, true);
add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(0), 144, false, 0, true);
}
diff --git a/src/wallet/test/db_tests.cpp b/src/wallet/test/db_tests.cpp
index adbbb94318..438dfceb7f 100644
--- a/src/wallet/test/db_tests.cpp
+++ b/src/wallet/test/db_tests.cpp
@@ -2,6 +2,8 @@
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+#include <config/bitcoin-config.h> // IWYU pragma: keep
+
#include <boost/test/unit_test.hpp>
#include <test/util/setup_common.h>
@@ -228,6 +230,39 @@ BOOST_AUTO_TEST_CASE(db_availability_after_write_error)
}
}
+// Verify 'ErasePrefix' functionality using db keys similar to the ones used by the wallet.
+// Keys are in the form of std::pair<TYPE, ENTRY_ID>
+BOOST_AUTO_TEST_CASE(erase_prefix)
+{
+ const std::string key = "key";
+ const std::string key2 = "key2";
+ const std::string value = "value";
+ const std::string value2 = "value_2";
+ auto make_key = [](std::string type, std::string id) { return std::make_pair(type, id); };
+
+ for (const auto& database : TestDatabases(m_path_root)) {
+ std::unique_ptr<DatabaseBatch> batch = database->MakeBatch();
+
+ // Write two entries with the same key type prefix, a third one with a different prefix
+ // and a fourth one with the type-id values inverted
+ BOOST_CHECK(batch->Write(make_key(key, value), value));
+ BOOST_CHECK(batch->Write(make_key(key, value2), value2));
+ BOOST_CHECK(batch->Write(make_key(key2, value), value));
+ BOOST_CHECK(batch->Write(make_key(value, key), value));
+
+ // Erase the ones with the same prefix and verify result
+ BOOST_CHECK(batch->TxnBegin());
+ BOOST_CHECK(batch->ErasePrefix(DataStream() << key));
+ BOOST_CHECK(batch->TxnCommit());
+
+ BOOST_CHECK(!batch->Exists(make_key(key, value)));
+ BOOST_CHECK(!batch->Exists(make_key(key, value2)));
+ // Also verify that entries with a different prefix were not erased
+ BOOST_CHECK(batch->Exists(make_key(key2, value)));
+ BOOST_CHECK(batch->Exists(make_key(value, key)));
+ }
+}
+
#ifdef USE_SQLITE
// Test-only statement execution error
diff --git a/src/wallet/test/fuzz/coinselection.cpp b/src/wallet/test/fuzz/coinselection.cpp
index 87d419493b..331590df7f 100644
--- a/src/wallet/test/fuzz/coinselection.cpp
+++ b/src/wallet/test/fuzz/coinselection.cpp
@@ -11,6 +11,7 @@
#include <test/util/setup_common.h>
#include <wallet/coinselection.h>
+#include <numeric>
#include <vector>
namespace wallet {
@@ -77,6 +78,144 @@ static SelectionResult ManualSelection(std::vector<COutput>& utxos, const CAmoun
// Returns true if the result contains an error and the message is not empty
static bool HasErrorMsg(const util::Result<SelectionResult>& res) { return !util::ErrorString(res).empty(); }
+FUZZ_TARGET(coin_grinder)
+{
+ FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
+ std::vector<COutput> utxo_pool;
+
+ const CAmount target{fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(1, MAX_MONEY)};
+
+ FastRandomContext fast_random_context{ConsumeUInt256(fuzzed_data_provider)};
+ CoinSelectionParams coin_params{fast_random_context};
+ coin_params.m_subtract_fee_outputs = fuzzed_data_provider.ConsumeBool();
+ coin_params.m_long_term_feerate = CFeeRate{ConsumeMoney(fuzzed_data_provider, /*max=*/COIN)};
+ coin_params.m_effective_feerate = CFeeRate{ConsumeMoney(fuzzed_data_provider, /*max=*/COIN)};
+ coin_params.change_output_size = fuzzed_data_provider.ConsumeIntegralInRange<int>(10, 1000);
+ coin_params.change_spend_size = fuzzed_data_provider.ConsumeIntegralInRange<int>(10, 1000);
+ coin_params.m_cost_of_change= coin_params.m_effective_feerate.GetFee(coin_params.change_output_size) + coin_params.m_long_term_feerate.GetFee(coin_params.change_spend_size);
+ coin_params.m_change_fee = coin_params.m_effective_feerate.GetFee(coin_params.change_output_size);
+ // For other results to be comparable to SRD, we must align the change_target with SRD’s hardcoded behavior
+ coin_params.m_min_change_target = CHANGE_LOWER + coin_params.m_change_fee;
+
+ // Create some coins
+ CAmount total_balance{0};
+ CAmount max_spendable{0};
+ int next_locktime{0};
+ LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10000)
+ {
+ const int n_input{fuzzed_data_provider.ConsumeIntegralInRange<int>(0, 10)};
+ const int n_input_bytes{fuzzed_data_provider.ConsumeIntegralInRange<int>(41, 10000)};
+ const CAmount amount{fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(1, MAX_MONEY)};
+ if (total_balance + amount >= MAX_MONEY) {
+ break;
+ }
+ AddCoin(amount, n_input, n_input_bytes, ++next_locktime, utxo_pool, coin_params.m_effective_feerate);
+ total_balance += amount;
+ CAmount eff_value = amount - coin_params.m_effective_feerate.GetFee(n_input_bytes);
+ max_spendable += eff_value;
+ }
+
+ std::vector<OutputGroup> group_pos;
+ GroupCoins(fuzzed_data_provider, utxo_pool, coin_params, /*positive_only=*/true, group_pos);
+
+ // Run coinselection algorithms
+ auto result_cg = CoinGrinder(group_pos, target, coin_params.m_min_change_target, MAX_STANDARD_TX_WEIGHT);
+ if (target + coin_params.m_min_change_target > max_spendable || HasErrorMsg(result_cg)) return; // We only need to compare algorithms if CoinGrinder has a solution
+ assert(result_cg);
+ if (!result_cg->GetAlgoCompleted()) return; // Bail out if CoinGrinder solution is not optimal
+
+ auto result_srd = SelectCoinsSRD(group_pos, target, coin_params.m_change_fee, fast_random_context, MAX_STANDARD_TX_WEIGHT);
+ if (result_srd && result_srd->GetChange(CHANGE_LOWER, coin_params.m_change_fee) > 0) { // exclude any srd solutions that don’t have change, err on excluding
+ assert(result_srd->GetWeight() >= result_cg->GetWeight());
+ }
+
+ auto result_knapsack = KnapsackSolver(group_pos, target, coin_params.m_min_change_target, fast_random_context, MAX_STANDARD_TX_WEIGHT);
+ if (result_knapsack && result_knapsack->GetChange(CHANGE_LOWER, coin_params.m_change_fee) > 0) { // exclude any knapsack solutions that don’t have change, err on excluding
+ assert(result_knapsack->GetWeight() >= result_cg->GetWeight());
+ }
+}
+
+FUZZ_TARGET(coin_grinder_is_optimal)
+{
+ FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
+
+ FastRandomContext fast_random_context{ConsumeUInt256(fuzzed_data_provider)};
+ CoinSelectionParams coin_params{fast_random_context};
+ coin_params.m_subtract_fee_outputs = false;
+ // Set effective feerate up to MAX_MONEY sats per 1'000'000 vB (2'100'000'000 sat/vB = 21'000 BTC/kvB).
+ coin_params.m_effective_feerate = CFeeRate{ConsumeMoney(fuzzed_data_provider, MAX_MONEY), 1'000'000};
+ coin_params.m_min_change_target = ConsumeMoney(fuzzed_data_provider);
+
+ // Create some coins
+ CAmount max_spendable{0};
+ int next_locktime{0};
+ static constexpr unsigned max_output_groups{16};
+ std::vector<OutputGroup> group_pos;
+ LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), max_output_groups)
+ {
+ // With maximum m_effective_feerate and n_input_bytes = 1'000'000, input_fee <= MAX_MONEY.
+ const int n_input_bytes{fuzzed_data_provider.ConsumeIntegralInRange<int>(1, 1'000'000)};
+ // Only make UTXOs with positive effective value
+ const CAmount input_fee = coin_params.m_effective_feerate.GetFee(n_input_bytes);
+ // Ensure that each UTXO has at least an effective value of 1 sat
+ const CAmount eff_value{fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(1, MAX_MONEY + group_pos.size() - max_spendable - max_output_groups)};
+ const CAmount amount{eff_value + input_fee};
+ std::vector<COutput> temp_utxo_pool;
+
+ AddCoin(amount, /*n_input=*/0, n_input_bytes, ++next_locktime, temp_utxo_pool, coin_params.m_effective_feerate);
+ max_spendable += eff_value;
+
+ auto output_group = OutputGroup(coin_params);
+ output_group.Insert(std::make_shared<COutput>(temp_utxo_pool.at(0)), /*ancestors=*/0, /*descendants=*/0);
+ group_pos.push_back(output_group);
+ }
+ size_t num_groups = group_pos.size();
+ assert(num_groups <= max_output_groups);
+
+ // Only choose targets below max_spendable
+ const CAmount target{fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(1, std::max(CAmount{1}, max_spendable - coin_params.m_min_change_target))};
+
+ // Brute force optimal solution
+ CAmount best_amount{MAX_MONEY};
+ int best_weight{std::numeric_limits<int>::max()};
+ for (uint32_t pattern = 1; (pattern >> num_groups) == 0; ++pattern) {
+ CAmount subset_amount{0};
+ int subset_weight{0};
+ for (unsigned i = 0; i < num_groups; ++i) {
+ if ((pattern >> i) & 1) {
+ subset_amount += group_pos[i].GetSelectionAmount();
+ subset_weight += group_pos[i].m_weight;
+ }
+ }
+ if ((subset_amount >= target + coin_params.m_min_change_target) && (subset_weight < best_weight || (subset_weight == best_weight && subset_amount < best_amount))) {
+ best_weight = subset_weight;
+ best_amount = subset_amount;
+ }
+ }
+
+ if (best_weight < std::numeric_limits<int>::max()) {
+ // Sufficient funds and acceptable weight: CoinGrinder should find at least one solution
+ int high_max_weight = fuzzed_data_provider.ConsumeIntegralInRange<int>(best_weight, std::numeric_limits<int>::max());
+
+ auto result_cg = CoinGrinder(group_pos, target, coin_params.m_min_change_target, high_max_weight);
+ assert(result_cg);
+ assert(result_cg->GetWeight() <= high_max_weight);
+ assert(result_cg->GetSelectedEffectiveValue() >= target + coin_params.m_min_change_target);
+ assert(best_weight < result_cg->GetWeight() || (best_weight == result_cg->GetWeight() && best_amount <= result_cg->GetSelectedEffectiveValue()));
+ if (result_cg->GetAlgoCompleted()) {
+ // If CoinGrinder exhausted the search space, it must return the optimal solution
+ assert(best_weight == result_cg->GetWeight());
+ assert(best_amount == result_cg->GetSelectedEffectiveValue());
+ }
+ }
+
+ // CoinGrinder cannot ever find a better solution than the brute-forced best, or there is none in the first place
+ int low_max_weight = fuzzed_data_provider.ConsumeIntegralInRange<int>(0, best_weight - 1);
+ auto result_cg = CoinGrinder(group_pos, target, coin_params.m_min_change_target, low_max_weight);
+ // Max_weight should have been exceeded, or there were insufficient funds
+ assert(!result_cg);
+}
+
FUZZ_TARGET(coinselection)
{
FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
@@ -152,7 +291,10 @@ FUZZ_TARGET(coinselection)
}
std::vector<COutput> utxos;
- std::vector<util::Result<SelectionResult>> results{result_srd, result_knapsack, result_bnb};
+ std::vector<util::Result<SelectionResult>> results;
+ results.emplace_back(std::move(result_srd));
+ results.emplace_back(std::move(result_knapsack));
+ results.emplace_back(std::move(result_bnb));
CAmount new_total_balance{CreateCoins(fuzzed_data_provider, utxos, coin_params, next_locktime)};
if (new_total_balance > 0) {
std::set<std::shared_ptr<COutput>> new_utxo_pool;
diff --git a/src/wallet/test/fuzz/notifications.cpp b/src/wallet/test/fuzz/notifications.cpp
index 9a515828fe..792079e6c6 100644
--- a/src/wallet/test/fuzz/notifications.cpp
+++ b/src/wallet/test/fuzz/notifications.cpp
@@ -106,13 +106,11 @@ struct FuzzedWallet {
CTxDestination GetDestination(FuzzedDataProvider& fuzzed_data_provider)
{
auto type{fuzzed_data_provider.PickValueInArray(OUTPUT_TYPES)};
- util::Result<CTxDestination> op_dest{util::Error{}};
if (fuzzed_data_provider.ConsumeBool()) {
- op_dest = wallet->GetNewDestination(type, "");
+ return *Assert(wallet->GetNewDestination(type, ""));
} else {
- op_dest = wallet->GetNewChangeDestination(type);
+ return *Assert(wallet->GetNewChangeDestination(type));
}
- return *Assert(op_dest);
}
CScript GetScriptPubKey(FuzzedDataProvider& fuzzed_data_provider) { return GetScriptForDestination(GetDestination(fuzzed_data_provider)); }
void FundTx(FuzzedDataProvider& fuzzed_data_provider, CMutableTransaction tx)
diff --git a/src/wallet/test/spend_tests.cpp b/src/wallet/test/spend_tests.cpp
index 3509bc116f..963c0f838b 100644
--- a/src/wallet/test/spend_tests.cpp
+++ b/src/wallet/test/spend_tests.cpp
@@ -97,13 +97,11 @@ BOOST_FIXTURE_TEST_CASE(wallet_duplicated_preset_inputs_test, TestChain100Setup)
// so that the recipient's amount is no longer equal to the user's selected target of 299 BTC.
// First case, use 'subtract_fee_from_outputs=true'
- util::Result<CreatedTransactionResult> res_tx = CreateTransaction(*wallet, recipients, /*change_pos=*/std::nullopt, coin_control);
- BOOST_CHECK(!res_tx.has_value());
+ BOOST_CHECK(!CreateTransaction(*wallet, recipients, /*change_pos=*/std::nullopt, coin_control));
// Second case, don't use 'subtract_fee_from_outputs'.
recipients[0].fSubtractFeeFromAmount = false;
- res_tx = CreateTransaction(*wallet, recipients, /*change_pos=*/std::nullopt, coin_control);
- BOOST_CHECK(!res_tx.has_value());
+ BOOST_CHECK(!CreateTransaction(*wallet, recipients, /*change_pos=*/std::nullopt, coin_control));
}
BOOST_AUTO_TEST_SUITE_END()
diff --git a/src/wallet/test/util.cpp b/src/wallet/test/util.cpp
index cbf3ccd1ec..49d206f409 100644
--- a/src/wallet/test/util.cpp
+++ b/src/wallet/test/util.cpp
@@ -71,7 +71,8 @@ std::shared_ptr<CWallet> TestLoadWallet(WalletContext& context)
void TestUnloadWallet(std::shared_ptr<CWallet>&& wallet)
{
- SyncWithValidationInterfaceQueue();
+ // Calls SyncWithValidationInterfaceQueue
+ wallet->chain().waitForNotificationsIfTipChanged({});
wallet->m_chain_notifications_handler.reset();
UnloadWallet(std::move(wallet));
}
diff --git a/src/wallet/test/util.h b/src/wallet/test/util.h
index 8bd238648f..a3e6ede81e 100644
--- a/src/wallet/test/util.h
+++ b/src/wallet/test/util.h
@@ -5,6 +5,8 @@
#ifndef BITCOIN_WALLET_TEST_UTIL_H
#define BITCOIN_WALLET_TEST_UTIL_H
+#include <config/bitcoin-config.h> // IWYU pragma: keep
+
#include <addresstype.h>
#include <wallet/db.h>
diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp
index 65297054df..3a67b9a433 100644
--- a/src/wallet/test/wallet_tests.cpp
+++ b/src/wallet/test/wallet_tests.cpp
@@ -445,9 +445,11 @@ BOOST_FIXTURE_TEST_CASE(LoadReceiveRequests, TestingSetup)
auto requests = wallet->GetAddressReceiveRequests();
auto erequests = {"val_rr11", "val_rr20"};
BOOST_CHECK_EQUAL_COLLECTIONS(requests.begin(), requests.end(), std::begin(erequests), std::end(erequests));
- WalletBatch batch{wallet->GetDatabase()};
- BOOST_CHECK(batch.WriteAddressPreviouslySpent(PKHash(), false));
- BOOST_CHECK(batch.EraseAddressData(ScriptHash()));
+ RunWithinTxn(wallet->GetDatabase(), /*process_desc*/"test", [](WalletBatch& batch){
+ BOOST_CHECK(batch.WriteAddressPreviouslySpent(PKHash(), false));
+ BOOST_CHECK(batch.EraseAddressData(ScriptHash()));
+ return true;
+ });
});
TestLoadWallet(name, format, [](std::shared_ptr<CWallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet) {
BOOST_CHECK(!wallet->IsAddressPreviouslySpent(PKHash()));
@@ -812,7 +814,7 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup)
// transactionAddedToMempool notifications, and create block and mempool
// transactions paying to the wallet
std::promise<void> promise;
- CallFunctionInValidationInterfaceQueue([&promise] {
+ m_node.validation_signals->CallFunctionInValidationInterfaceQueue([&promise] {
promise.get_future().wait();
});
std::string error;
@@ -840,7 +842,7 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup)
// Unblock notification queue and make sure stale blockConnected and
// transactionAddedToMempool events are processed
promise.set_value();
- SyncWithValidationInterfaceQueue();
+ m_node.validation_signals->SyncWithValidationInterfaceQueue();
// AddToWallet events for block_tx and mempool_tx events are counted a
// second time as the notification queue is processed
BOOST_CHECK_EQUAL(addtx_count, 5);
@@ -863,7 +865,7 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup)
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();
+ m_node.validation_signals->SyncWithValidationInterfaceQueue();
});
wallet = TestLoadWallet(context);
// Since mempool transactions are requested at the end of loading, there will
@@ -888,7 +890,7 @@ BOOST_FIXTURE_TEST_CASE(CreateWalletWithoutChain, BasicTestingSetup)
UnloadWallet(std::move(wallet));
}
-BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup)
+BOOST_FIXTURE_TEST_CASE(RemoveTxs, TestChain100Setup)
{
m_args.ForceSetArg("-unsafesqlitesync", "1");
WalletContext context;
@@ -903,7 +905,7 @@ BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup)
auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
- SyncWithValidationInterfaceQueue();
+ m_node.validation_signals->SyncWithValidationInterfaceQueue();
{
auto block_hash = block_tx.GetHash();
@@ -913,8 +915,8 @@ BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup)
BOOST_CHECK(wallet->HasWalletSpend(prev_tx));
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 1u);
- std::vector<uint256> vHashIn{ block_hash }, vHashOut;
- BOOST_CHECK_EQUAL(wallet->ZapSelectTx(vHashIn, vHashOut), DBErrors::LOAD_OK);
+ std::vector<uint256> vHashIn{ block_hash };
+ BOOST_CHECK(wallet->RemoveTxs(vHashIn));
BOOST_CHECK(!wallet->HasWalletSpend(prev_tx));
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 0u);
diff --git a/src/wallet/test/walletload_tests.cpp b/src/wallet/test/walletload_tests.cpp
index 3dba2231f0..2e43eda582 100644
--- a/src/wallet/test/walletload_tests.cpp
+++ b/src/wallet/test/walletload_tests.cpp
@@ -34,6 +34,7 @@ public:
std::optional<int64_t> ScriptSize() const override { return {}; }
std::optional<int64_t> MaxSatisfactionWeight(bool) const override { return {}; }
std::optional<int64_t> MaxSatisfactionElems() const override { return {}; }
+ void GetPubKeys(std::set<CPubKey>& pubkeys, std::set<CExtPubKey>& ext_pubs) const override {}
};
BOOST_FIXTURE_TEST_CASE(wallet_load_descriptors, TestingSetup)