Skip to content

Commit 3f8591d

Browse files
committed
Merge bitcoin/bitcoin#26661: wallet: Coin Selection, return accurate error messages
76dc547 gui: create tx, launch error dialog if backend throws runtime_error (furszy) f4d7947 wallet: coin selection, add duplicated inputs checks (furszy) 0aa065b wallet: return accurate error messages from Coin Selection (furszy) 7e8340a wallet: make SelectCoins flow return util::Result (furszy) e5e147f wallet: refactor eight consecutive 'AttemptSelection' calls into a loop (furszy) Pull request description: Work decoupled from #25806, which cleanup and improves the Coin Selection flow further. Adding the capability to propagate specific error messages from the Coin Selection process to the user. Instead of always returning the general "Insufficient funds" message which is not always accurate to what happened internally. Letting us instruct the user how to proceed under certain circumstances. The following error messages were added: 1) If the selection result exceeds the maximum transaction weight, we now will return: -> "The inputs size exceeds the maximum weight. Please try sending a smaller amount or manually consolidating your wallet's UTXOs". 2) If the user pre-selected inputs and disallowed the automatic coin selection process (no other inputs are allowed), we now will return: -> "The preselected coins total amount does not cover the transaction target. Please allow other inputs to be automatically selected or include more coins manually". 3) The double-counted preset inputs during Coin Selection error will now throw an "internal bug detected" message instead of crashing the node. The essence of this work comes from several comments: 1. bitcoin/bitcoin#26560 (comment) 2. bitcoin/bitcoin#25729 (comment) 3. bitcoin/bitcoin#25269 (review) 4. bitcoin/bitcoin#23144 (which is connected to #24845) ACKs for top commit: ishaanam: crACK 76dc547 achow101: ACK 76dc547 aureleoules: ACK 76dc547 theStack: ACK 76dc547 🌇 Tree-SHA512: 9de30792d7a5849cae77747aa978e70390b66ee9d082779a56088a024f82e725b0af050e6603aece0ac8229f6d73bc471ba97b4ab69dc7eddf419f5f56ae89a5
2 parents 80fc1af + 76dc547 commit 3f8591d

File tree

9 files changed

+135
-85
lines changed

9 files changed

+135
-85
lines changed

src/qt/walletmodel.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransact
212212
return AmountExceedsBalance;
213213
}
214214

215-
{
215+
try {
216216
CAmount nFeeRequired = 0;
217217
int nChangePosRet = -1;
218218

@@ -240,6 +240,11 @@ WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransact
240240
if (nFeeRequired > m_wallet->getDefaultMaxTxFee()) {
241241
return AbsurdFee;
242242
}
243+
} catch (const std::runtime_error& err) {
244+
// Something unexpected happened, instruct user to report this bug.
245+
Q_EMIT message(tr("Send Coins"), QString::fromStdString(err.what()),
246+
CClientUIInterface::MSG_ERROR);
247+
return TransactionCreationFailed;
243248
}
244249

245250
return SendCoinsReturn(OK);

src/wallet/coinselection.cpp

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -448,15 +448,17 @@ void SelectionResult::Clear()
448448

449449
void SelectionResult::AddInput(const OutputGroup& group)
450450
{
451-
util::insert(m_selected_inputs, group.m_outputs);
451+
// As it can fail, combine inputs first
452+
InsertInputs(group.m_outputs);
452453
m_use_effective = !group.m_subtract_fee_outputs;
453454

454455
m_weight += group.m_weight;
455456
}
456457

457458
void SelectionResult::AddInputs(const std::set<COutput>& inputs, bool subtract_fee_outputs)
458459
{
459-
util::insert(m_selected_inputs, inputs);
460+
// As it can fail, combine inputs first
461+
InsertInputs(inputs);
460462
m_use_effective = !subtract_fee_outputs;
461463

462464
m_weight += std::accumulate(inputs.cbegin(), inputs.cend(), 0, [](int sum, const auto& coin) {
@@ -466,16 +468,14 @@ void SelectionResult::AddInputs(const std::set<COutput>& inputs, bool subtract_f
466468

467469
void SelectionResult::Merge(const SelectionResult& other)
468470
{
469-
// Obtain the expected selected inputs count after the merge (for now, duplicates are not allowed)
470-
const size_t expected_count = m_selected_inputs.size() + other.m_selected_inputs.size();
471+
// As it can fail, combine inputs first
472+
InsertInputs(other.m_selected_inputs);
471473

472474
m_target += other.m_target;
473475
m_use_effective |= other.m_use_effective;
474476
if (m_algo == SelectionAlgorithm::MANUAL) {
475477
m_algo = other.m_algo;
476478
}
477-
util::insert(m_selected_inputs, other.m_selected_inputs);
478-
assert(m_selected_inputs.size() == expected_count);
479479

480480
m_weight += other.m_weight;
481481
}

src/wallet/coinselection.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
#include <policy/feerate.h>
1111
#include <primitives/transaction.h>
1212
#include <random.h>
13+
#include <util/system.h>
14+
#include <util/check.h>
1315

1416
#include <optional>
1517

@@ -186,6 +188,7 @@ struct CoinEligibilityFilter
186188
/** When avoid_reuse=true and there are full groups (OUTPUT_GROUP_MAX_ENTRIES), whether or not to use any partial groups.*/
187189
const bool m_include_partial_groups{false};
188190

191+
CoinEligibilityFilter() = delete;
189192
CoinEligibilityFilter(int conf_mine, int conf_theirs, uint64_t max_ancestors) : conf_mine(conf_mine), conf_theirs(conf_theirs), max_ancestors(max_ancestors), max_descendants(max_ancestors) {}
190193
CoinEligibilityFilter(int conf_mine, int conf_theirs, uint64_t max_ancestors, uint64_t max_descendants) : conf_mine(conf_mine), conf_theirs(conf_theirs), max_ancestors(max_ancestors), max_descendants(max_descendants) {}
191194
CoinEligibilityFilter(int conf_mine, int conf_theirs, uint64_t max_ancestors, uint64_t max_descendants, bool include_partial) : conf_mine(conf_mine), conf_theirs(conf_theirs), max_ancestors(max_ancestors), max_descendants(max_descendants), m_include_partial_groups(include_partial) {}
@@ -301,6 +304,17 @@ struct SelectionResult
301304
/** Total weight of the selected inputs */
302305
int m_weight{0};
303306

307+
template<typename T>
308+
void InsertInputs(const T& inputs)
309+
{
310+
// Store sum of combined input sets to check that the results have no shared UTXOs
311+
const size_t expected_count = m_selected_inputs.size() + inputs.size();
312+
util::insert(m_selected_inputs, inputs);
313+
if (m_selected_inputs.size() != expected_count) {
314+
throw std::runtime_error(STR_INTERNAL_BUG("Shared UTXOs among selection results"));
315+
}
316+
}
317+
304318
public:
305319
explicit SelectionResult(const CAmount target, SelectionAlgorithm algo)
306320
: m_target(target), m_algo(algo) {}

src/wallet/spend.cpp

Lines changed: 74 additions & 57 deletions
Large diffs are not rendered by default.

src/wallet/spend.h

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,11 @@ std::vector<OutputGroup> GroupOutputs(const CWallet& wallet, const std::vector<C
121121
* param@[in] coin_selection_params Parameters for the coin selection
122122
* param@[in] allow_mixed_output_types Relax restriction that SelectionResults must be of the same OutputType
123123
* returns If successful, a SelectionResult containing the input set
124-
* If failed, a nullopt
124+
* If failed, returns (1) an empty error message if the target was not reached (general "Insufficient funds")
125+
* or (2) an specific error message if there was something particularly wrong (e.g. a selection
126+
* result that surpassed the tx max weight size).
125127
*/
126-
std::optional<SelectionResult> AttemptSelection(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, const CoinsResult& available_coins,
128+
util::Result<SelectionResult> AttemptSelection(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, const CoinsResult& available_coins,
127129
const CoinSelectionParams& coin_selection_params, bool allow_mixed_output_types);
128130

129131
/**
@@ -137,9 +139,11 @@ std::optional<SelectionResult> AttemptSelection(const CWallet& wallet, const CAm
137139
* param@[in] available_coins The struct of coins, organized by OutputType, available for selection prior to filtering
138140
* param@[in] coin_selection_params Parameters for the coin selection
139141
* returns If successful, a SelectionResult containing the input set
140-
* If failed, a nullopt
142+
* If failed, returns (1) an empty error message if the target was not reached (general "Insufficient funds")
143+
* or (2) an specific error message if there was something particularly wrong (e.g. a selection
144+
* result that surpassed the tx max weight size).
141145
*/
142-
std::optional<SelectionResult> ChooseSelectionResult(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, const std::vector<COutput>& available_coins,
146+
util::Result<SelectionResult> ChooseSelectionResult(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, const std::vector<COutput>& available_coins,
143147
const CoinSelectionParams& coin_selection_params);
144148

145149
// User manually selected inputs that must be part of the transaction
@@ -177,18 +181,20 @@ util::Result<PreSelectedInputs> FetchSelectedInputs(const CWallet& wallet, const
177181
* param@[in] coin_selection_params Parameters for this coin selection such as feerates, whether to avoid partial spends,
178182
* and whether to subtract the fee from the outputs.
179183
* returns If successful, a SelectionResult containing the selected coins
180-
* If failed, a nullopt.
184+
* If failed, returns (1) an empty error message if the target was not reached (general "Insufficient funds")
185+
* or (2) an specific error message if there was something particularly wrong (e.g. a selection
186+
* result that surpassed the tx max weight size).
181187
*/
182-
std::optional<SelectionResult> AutomaticCoinSelection(const CWallet& wallet, CoinsResult& available_coins, const CAmount& nTargetValue, const CCoinControl& coin_control,
188+
util::Result<SelectionResult> AutomaticCoinSelection(const CWallet& wallet, CoinsResult& available_coins, const CAmount& nTargetValue, const CCoinControl& coin_control,
183189
const CoinSelectionParams& coin_selection_params) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
184190

185191
/**
186192
* Select all coins from coin_control, and if coin_control 'm_allow_other_inputs=true', call 'AutomaticCoinSelection' to
187193
* select a set of coins such that nTargetValue - pre_set_inputs.total_amount is met.
188194
*/
189-
std::optional<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& available_coins, const PreSelectedInputs& pre_set_inputs,
190-
const CAmount& nTargetValue, const CCoinControl& coin_control,
191-
const CoinSelectionParams& coin_selection_params) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
195+
util::Result<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& available_coins, const PreSelectedInputs& pre_set_inputs,
196+
const CAmount& nTargetValue, const CCoinControl& coin_control,
197+
const CoinSelectionParams& coin_selection_params) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
192198

193199
struct CreatedTransactionResult
194200
{

src/wallet/test/coinselector_tests.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -931,7 +931,7 @@ BOOST_AUTO_TEST_CASE(effective_value_test)
931931
BOOST_CHECK_EQUAL(output5.GetEffectiveValue(), nValue); // The effective value should be equal to the absolute value if input_bytes is -1
932932
}
933933

934-
static std::optional<SelectionResult> select_coins(const CAmount& target, const CoinSelectionParams& cs_params, const CCoinControl& cc, std::function<CoinsResult(CWallet&)> coin_setup, interfaces::Chain* chain, const ArgsManager& args)
934+
static util::Result<SelectionResult> select_coins(const CAmount& target, const CoinSelectionParams& cs_params, const CCoinControl& cc, std::function<CoinsResult(CWallet&)> coin_setup, interfaces::Chain* chain, const ArgsManager& args)
935935
{
936936
std::unique_ptr<CWallet> wallet = std::make_unique<CWallet>(chain, "", args, CreateMockWalletDatabase());
937937
wallet->LoadWallet();

test/functional/rpc_psbt.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@ def run_test(self):
120120

121121
# If inputs are specified, do not automatically add more:
122122
utxo1 = self.nodes[0].listunspent()[0]
123-
assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[0].walletcreatefundedpsbt, [{"txid": utxo1['txid'], "vout": utxo1['vout']}], {self.nodes[2].getnewaddress():90})
123+
assert_raises_rpc_error(-4, "The preselected coins total amount does not cover the transaction target. "
124+
"Please allow other inputs to be automatically selected or include more coins manually",
125+
self.nodes[0].walletcreatefundedpsbt, [{"txid": utxo1['txid'], "vout": utxo1['vout']}], {self.nodes[2].getnewaddress():90})
124126

125127
psbtx1 = self.nodes[0].walletcreatefundedpsbt([{"txid": utxo1['txid'], "vout": utxo1['vout']}], {self.nodes[2].getnewaddress():90}, 0, {"add_inputs": True})['psbt']
126128
assert_equal(len(self.nodes[0].decodepsbt(psbtx1)['tx']['vin']), 2)

test/functional/wallet_fundrawtransaction.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
)
2828
from test_framework.wallet_util import bytes_to_wif
2929

30+
ERR_NOT_ENOUGH_PRESET_INPUTS = "The preselected coins total amount does not cover the transaction target. " \
31+
"Please allow other inputs to be automatically selected or include more coins manually"
3032

3133
def get_unspent(listunspent, amount):
3234
for utx in listunspent:
@@ -328,7 +330,7 @@ def test_coin_selection(self):
328330
assert_equal("00", dec_tx['vin'][0]['scriptSig']['hex'])
329331

330332
# Should fail without add_inputs:
331-
assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[2].fundrawtransaction, rawtx, {"add_inputs": False})
333+
assert_raises_rpc_error(-4, ERR_NOT_ENOUGH_PRESET_INPUTS, self.nodes[2].fundrawtransaction, rawtx, {"add_inputs": False})
332334
# add_inputs is enabled by default
333335
rawtxfund = self.nodes[2].fundrawtransaction(rawtx)
334336

@@ -360,7 +362,7 @@ def test_two_vin(self):
360362
assert_equal(utx['txid'], dec_tx['vin'][0]['txid'])
361363

362364
# Should fail without add_inputs:
363-
assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[2].fundrawtransaction, rawtx, {"add_inputs": False})
365+
assert_raises_rpc_error(-4, ERR_NOT_ENOUGH_PRESET_INPUTS, self.nodes[2].fundrawtransaction, rawtx, {"add_inputs": False})
364366
rawtxfund = self.nodes[2].fundrawtransaction(rawtx, {"add_inputs": True})
365367

366368
dec_tx = self.nodes[2].decoderawtransaction(rawtxfund['hex'])
@@ -394,7 +396,7 @@ def test_two_vin_two_vout(self):
394396
assert_equal(utx['txid'], dec_tx['vin'][0]['txid'])
395397

396398
# Should fail without add_inputs:
397-
assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[2].fundrawtransaction, rawtx, {"add_inputs": False})
399+
assert_raises_rpc_error(-4, ERR_NOT_ENOUGH_PRESET_INPUTS, self.nodes[2].fundrawtransaction, rawtx, {"add_inputs": False})
398400
rawtxfund = self.nodes[2].fundrawtransaction(rawtx, {"add_inputs": True})
399401

400402
dec_tx = self.nodes[2].decoderawtransaction(rawtxfund['hex'])
@@ -989,7 +991,9 @@ def test_transaction_too_large(self):
989991
outputs[recipient.getnewaddress()] = 0.1
990992
wallet.sendmany("", outputs)
991993
self.generate(self.nodes[0], 10)
992-
assert_raises_rpc_error(-4, "Insufficient funds", recipient.fundrawtransaction, rawtx)
994+
assert_raises_rpc_error(-4, "The inputs size exceeds the maximum weight. "
995+
"Please try sending a smaller amount or manually consolidating your wallet's UTXOs",
996+
recipient.fundrawtransaction, rawtx)
993997
self.nodes[0].unloadwallet("large")
994998

995999
def test_external_inputs(self):
@@ -1130,7 +1134,7 @@ def test_add_inputs_default_value(self):
11301134
}
11311135
]
11321136
}
1133-
assert_raises_rpc_error(-4, "Insufficient funds", wallet.send, outputs=[{addr1: 8}], options=options)
1137+
assert_raises_rpc_error(-4, ERR_NOT_ENOUGH_PRESET_INPUTS, wallet.send, outputs=[{addr1: 8}], options=options)
11341138

11351139
# Case (3), Explicit add_inputs=true and preset inputs (with preset inputs not-covering the target amount)
11361140
options["add_inputs"] = True
@@ -1158,7 +1162,7 @@ def test_add_inputs_default_value(self):
11581162

11591163
# 6. Explicit add_inputs=false, no preset inputs:
11601164
options = {"add_inputs": False}
1161-
assert_raises_rpc_error(-4, "Insufficient funds", wallet.send, outputs=[{addr1: 3}], options=options)
1165+
assert_raises_rpc_error(-4, ERR_NOT_ENOUGH_PRESET_INPUTS, wallet.send, outputs=[{addr1: 3}], options=options)
11621166

11631167
################################################
11641168

@@ -1175,7 +1179,7 @@ def test_add_inputs_default_value(self):
11751179
"vout": 1 # change position was hardcoded to index 0
11761180
}]
11771181
outputs = {self.nodes[1].getnewaddress(): 8}
1178-
assert_raises_rpc_error(-4, "Insufficient funds", wallet.walletcreatefundedpsbt, inputs=inputs, outputs=outputs)
1182+
assert_raises_rpc_error(-4, ERR_NOT_ENOUGH_PRESET_INPUTS, wallet.walletcreatefundedpsbt, inputs=inputs, outputs=outputs)
11791183

11801184
# Case (3), Explicit add_inputs=true and preset inputs (with preset inputs not-covering the target amount)
11811185
options["add_inputs"] = True
@@ -1202,7 +1206,7 @@ def test_add_inputs_default_value(self):
12021206

12031207
# Case (6). Explicit add_inputs=false, no preset inputs:
12041208
options = {"add_inputs": False}
1205-
assert_raises_rpc_error(-4, "Insufficient funds", wallet.walletcreatefundedpsbt, inputs=[], outputs=outputs, options=options)
1209+
assert_raises_rpc_error(-4, ERR_NOT_ENOUGH_PRESET_INPUTS, wallet.walletcreatefundedpsbt, inputs=[], outputs=outputs, options=options)
12061210

12071211
self.nodes[2].unloadwallet("test_preset_inputs")
12081212

test/functional/wallet_send.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,10 +412,12 @@ def run_test(self):
412412
assert res["complete"]
413413
utxo1 = w0.listunspent()[0]
414414
assert_equal(utxo1["amount"], 50)
415+
ERR_NOT_ENOUGH_PRESET_INPUTS = "The preselected coins total amount does not cover the transaction target. " \
416+
"Please allow other inputs to be automatically selected or include more coins manually"
415417
self.test_send(from_wallet=w0, to_wallet=w1, amount=51, inputs=[utxo1],
416-
expect_error=(-4, "Insufficient funds"))
418+
expect_error=(-4, ERR_NOT_ENOUGH_PRESET_INPUTS))
417419
self.test_send(from_wallet=w0, to_wallet=w1, amount=51, inputs=[utxo1], add_inputs=False,
418-
expect_error=(-4, "Insufficient funds"))
420+
expect_error=(-4, ERR_NOT_ENOUGH_PRESET_INPUTS))
419421
res = self.test_send(from_wallet=w0, to_wallet=w1, amount=51, inputs=[utxo1], add_inputs=True, add_to_wallet=False)
420422
assert res["complete"]
421423

0 commit comments

Comments
 (0)