Skip to content

Commit 381593c

Browse files
committed
Merge bitcoin/bitcoin#24845: wallet: return error msg for "too-long-mempool-chain"
f3221d3 test: add wallet too-long-mempool-chain error coverage (furszy) acf0119 wallet: return error msg for too-long-mempool-chain failure (furszy) Pull request description: Fixes #23144. We currently return a general "Insufficient funds" from Coin Selection when we actually skipped unconfirmed UTXOs that surpassed the mempool ancestors limit. This PR make the error clearer by returning: "Unconfirmed UTXOs are available, but spending them creates a chain of transactions that will be rejected by the mempool" Also, added an early return from Coin Selection if the sum of the discarded coins decreases the available balance below the target amount. ACKs for top commit: achow101: ACK f3221d3 S3RK: Code review ACK f3221d3 Xekyo: ACK f3221d3 Tree-SHA512: 13e5824b75ac302280ff894560a4ebf32a74f32fe49ef8281f2bc99c0104b92cef33d3b143c6e131f3a07eafe64533af7fc60abff585142c134b9d6e531a6a66
2 parents 483fb8d + f3221d3 commit 381593c

File tree

2 files changed

+67
-4
lines changed

2 files changed

+67
-4
lines changed

src/wallet/spend.cpp

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,8 @@ std::map<CTxDestination, std::vector<COutput>> ListCoins(const CWallet& wallet)
407407
FilteredOutputGroups GroupOutputs(const CWallet& wallet,
408408
const CoinsResult& coins,
409409
const CoinSelectionParams& coin_sel_params,
410-
const std::vector<SelectionFilter>& filters)
410+
const std::vector<SelectionFilter>& filters,
411+
std::vector<OutputGroup>& ret_discarded_groups)
411412
{
412413
FilteredOutputGroups filtered_groups;
413414

@@ -424,11 +425,14 @@ FilteredOutputGroups GroupOutputs(const CWallet& wallet,
424425
group.Insert(std::make_shared<COutput>(output), ancestors, descendants);
425426

426427
// Each filter maps to a different set of groups
428+
bool accepted = false;
427429
for (const auto& sel_filter : filters) {
428430
const auto& filter = sel_filter.filter;
429431
if (!group.EligibleForSpending(filter)) continue;
430432
filtered_groups[filter].Push(group, type, /*insert_positive=*/true, /*insert_mixed=*/true);
433+
accepted = true;
431434
}
435+
if (!accepted) ret_discarded_groups.emplace_back(group);
432436
}
433437
}
434438
return filtered_groups;
@@ -492,6 +496,7 @@ FilteredOutputGroups GroupOutputs(const CWallet& wallet,
492496
const OutputGroup& group = *group_it;
493497

494498
// Each filter maps to a different set of groups
499+
bool accepted = false;
495500
for (const auto& sel_filter : filters) {
496501
const auto& filter = sel_filter.filter;
497502
if (!group.EligibleForSpending(filter)) continue;
@@ -504,7 +509,9 @@ FilteredOutputGroups GroupOutputs(const CWallet& wallet,
504509
OutputType type = script.second;
505510
// Either insert the group into the positive-only groups or the mixed ones.
506511
filtered_groups[filter].Push(group, type, positive_only, /*insert_mixed=*/!positive_only);
512+
accepted = true;
507513
}
514+
if (!accepted) ret_discarded_groups.emplace_back(group);
508515
}
509516
}
510517
};
@@ -515,6 +522,15 @@ FilteredOutputGroups GroupOutputs(const CWallet& wallet,
515522
return filtered_groups;
516523
}
517524

525+
FilteredOutputGroups GroupOutputs(const CWallet& wallet,
526+
const CoinsResult& coins,
527+
const CoinSelectionParams& params,
528+
const std::vector<SelectionFilter>& filters)
529+
{
530+
std::vector<OutputGroup> unused;
531+
return GroupOutputs(wallet, coins, params, filters, unused);
532+
}
533+
518534
// Returns true if the result contains an error and the message is not empty
519535
static bool HasErrorMsg(const util::Result<SelectionResult>& res) { return !util::ErrorString(res).empty(); }
520536

@@ -685,7 +701,24 @@ util::Result<SelectionResult> AutomaticCoinSelection(const CWallet& wallet, Coin
685701
}
686702

687703
// Group outputs and map them by coin eligibility filter
688-
FilteredOutputGroups filtered_groups = GroupOutputs(wallet, available_coins, coin_selection_params, ordered_filters);
704+
std::vector<OutputGroup> discarded_groups;
705+
FilteredOutputGroups filtered_groups = GroupOutputs(wallet, available_coins, coin_selection_params, ordered_filters, discarded_groups);
706+
707+
// Check if we still have enough balance after applying filters (some coins might be discarded)
708+
CAmount total_discarded = 0;
709+
CAmount total_unconf_long_chain = 0;
710+
for (const auto& group : discarded_groups) {
711+
total_discarded += group.GetSelectionAmount();
712+
if (group.m_ancestors >= max_ancestors || group.m_descendants >= max_descendants) total_unconf_long_chain += group.GetSelectionAmount();
713+
}
714+
715+
if (CAmount total_amount = available_coins.GetTotalAmount() - total_discarded < value_to_select) {
716+
// Special case, too-long-mempool cluster.
717+
if (total_amount + total_unconf_long_chain > value_to_select) {
718+
return util::Result<SelectionResult>({_("Unconfirmed UTXOs are available, but spending them creates a chain of transactions that will be rejected by the mempool")});
719+
}
720+
return util::Result<SelectionResult>(util::Error()); // General "Insufficient Funds"
721+
}
689722

690723
// Walk-through the filters until the solution gets found.
691724
// If no solution is found, return the first detailed error (if any).
@@ -704,8 +737,13 @@ util::Result<SelectionResult> AutomaticCoinSelection(const CWallet& wallet, Coin
704737
if (HasErrorMsg(res)) res_detailed_errors.emplace_back(res);
705738
}
706739
}
707-
// Coin Selection failed.
708-
return res_detailed_errors.empty() ? util::Result<SelectionResult>(util::Error()) : res_detailed_errors.front();
740+
741+
// Return right away if we have a detailed error
742+
if (!res_detailed_errors.empty()) return res_detailed_errors.front();
743+
744+
745+
// General "Insufficient Funds"
746+
return util::Result<SelectionResult>(util::Error());
709747
}();
710748

711749
return res;

test/functional/wallet_create_tx.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def run_test(self):
3232

3333
self.test_anti_fee_sniping()
3434
self.test_tx_size_too_large()
35+
self.test_create_too_long_mempool_chain()
3536

3637
def test_anti_fee_sniping(self):
3738
self.log.info('Check that we have some (old) blocks and that anti-fee-sniping is disabled')
@@ -80,6 +81,30 @@ def test_tx_size_too_large(self):
8081
)
8182
self.nodes[0].settxfee(0)
8283

84+
def test_create_too_long_mempool_chain(self):
85+
self.log.info('Check too-long mempool chain error')
86+
df_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
87+
88+
self.nodes[0].createwallet("too_long")
89+
test_wallet = self.nodes[0].get_wallet_rpc("too_long")
90+
91+
tx_data = df_wallet.send(outputs=[{test_wallet.getnewaddress(): 25}], options={"change_position": 0})
92+
txid = tx_data['txid']
93+
vout = 1
94+
95+
options = {"change_position": 0, "add_inputs": False}
96+
for i in range(1, 25):
97+
options['inputs'] = [{'txid': txid, 'vout': vout}]
98+
tx_data = test_wallet.send(outputs=[{test_wallet.getnewaddress(): 25 - i}], options=options)
99+
txid = tx_data['txid']
100+
101+
# Sending one more chained transaction will fail
102+
options = {"minconf": 0, "include_unsafe": True, 'add_inputs': True}
103+
assert_raises_rpc_error(-4, "Unconfirmed UTXOs are available, but spending them creates a chain of transactions that will be rejected by the mempool",
104+
test_wallet.send, outputs=[{test_wallet.getnewaddress(): 0.3}], options=options)
105+
106+
test_wallet.unloadwallet()
107+
83108

84109
if __name__ == '__main__':
85110
CreateTxWalletTest().main()

0 commit comments

Comments
 (0)