Skip to content

Commit a5916a8

Browse files
committed
bumpfee: Allow original change position to be specified
If the user used a custom change address, it may not be detected as a change output, resulting in an additional change output being added to the bumped transaction. We can avoid this issue by allowing the user to specify the position of the change output.
1 parent 5394522 commit a5916a8

File tree

3 files changed

+38
-9
lines changed

3 files changed

+38
-9
lines changed

src/wallet/feebumper.cpp

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,14 @@ bool TransactionCanBeBumped(const CWallet& wallet, const uint256& txid)
152152
}
153153

154154
Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCoinControl& coin_control, std::vector<bilingual_str>& errors,
155-
CAmount& old_fee, CAmount& new_fee, CMutableTransaction& mtx, bool require_mine, const std::vector<CTxOut>& outputs)
155+
CAmount& old_fee, CAmount& new_fee, CMutableTransaction& mtx, bool require_mine, const std::vector<CTxOut>& outputs, std::optional<uint32_t> reduce_output)
156156
{
157+
// Cannot both specify new outputs and an output to reduce
158+
if (!outputs.empty() && reduce_output.has_value()) {
159+
errors.push_back(Untranslated("Cannot specify both new outputs to use and an output index to reduce"));
160+
return Result::INVALID_PARAMETER;
161+
}
162+
157163
// We are going to modify coin control later, copy to re-use
158164
CCoinControl new_coin_control(coin_control);
159165

@@ -166,6 +172,12 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo
166172
}
167173
const CWalletTx& wtx = it->second;
168174

175+
// Make sure that reduce_output is valid
176+
if (reduce_output.has_value() && reduce_output.value() >= wtx.tx->vout.size()) {
177+
errors.push_back(Untranslated("Change position is out of range"));
178+
return Result::INVALID_PARAMETER;
179+
}
180+
169181
// Retrieve all of the UTXOs and add them to coin control
170182
// While we're here, calculate the input amount
171183
std::map<COutPoint, Coin> coins;
@@ -233,14 +245,15 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo
233245
std::vector<CRecipient> recipients;
234246
CAmount new_outputs_value = 0;
235247
const auto& txouts = outputs.empty() ? wtx.tx->vout : outputs;
236-
for (const auto& output : txouts) {
237-
if (!OutputIsChange(wallet, output)) {
238-
CRecipient recipient = {output.scriptPubKey, output.nValue, false};
239-
recipients.push_back(recipient);
240-
} else {
248+
for (size_t i = 0; i < txouts.size(); ++i) {
249+
const CTxOut& output = txouts.at(i);
250+
if (reduce_output.has_value() ? reduce_output.value() == i : OutputIsChange(wallet, output)) {
241251
CTxDestination change_dest;
242252
ExtractDestination(output.scriptPubKey, change_dest);
243253
new_coin_control.destChange = change_dest;
254+
} else {
255+
CRecipient recipient = {output.scriptPubKey, output.nValue, false};
256+
recipients.push_back(recipient);
244257
}
245258
new_outputs_value += output.nValue;
246259
}

src/wallet/feebumper.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ bool TransactionCanBeBumped(const CWallet& wallet, const uint256& txid);
4343
* @param[out] new_fee the fee that the bump transaction pays
4444
* @param[out] mtx The bump transaction itself
4545
* @param[in] require_mine Whether the original transaction must consist of inputs that can be spent by the wallet
46+
* #param[in] outputs Vector of new outputs to replace the bumped transaction's outputs
47+
* @param[in] reduce_output The position of the change output to deduct the fee from in the transaction being bumped
4648
*/
4749
Result CreateRateBumpTransaction(CWallet& wallet,
4850
const uint256& txid,
@@ -52,7 +54,8 @@ Result CreateRateBumpTransaction(CWallet& wallet,
5254
CAmount& new_fee,
5355
CMutableTransaction& mtx,
5456
bool require_mine,
55-
const std::vector<CTxOut>& outputs);
57+
const std::vector<CTxOut>& outputs,
58+
std::optional<uint32_t> reduce_output = std::nullopt);
5659

5760
//! Sign the new transaction,
5861
//! @return false if the tx couldn't be found or if it was

src/wallet/rpc/spend.cpp

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,9 +1015,11 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
10151015
"\"" + FeeModes("\"\n\"") + "\""},
10161016
{"outputs", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "New outputs (key-value pairs) which will replace\n"
10171017
"the original ones, if provided. Each address can only appear once and there can\n"
1018-
"only be one \"data\" object.\n",
1018+
"only be one \"data\" object.\n"
1019+
"Cannot be provided if 'reduce_output' is specified.",
10191020
OutputsDoc(),
10201021
RPCArgOptions{.skip_type_check = true}},
1022+
{"reduce_output", RPCArg::Type::NUM, RPCArg::DefaultHint{"not set, detect change automatically"}, "The 0-based index of the output from which the additional fees will be deducted. In general, this should be the position of change output. Cannot be provided if 'outputs' is specified."},
10211023
},
10221024
RPCArgOptions{.oneline_description="options"}},
10231025
},
@@ -1056,6 +1058,8 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
10561058
coin_control.m_signal_bip125_rbf = true;
10571059
std::vector<CTxOut> outputs;
10581060

1061+
std::optional<uint32_t> reduce_output;
1062+
10591063
if (!request.params[1].isNull()) {
10601064
UniValue options = request.params[1];
10611065
RPCTypeCheckObj(options,
@@ -1066,6 +1070,7 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
10661070
{"replaceable", UniValueType(UniValue::VBOOL)},
10671071
{"estimate_mode", UniValueType(UniValue::VSTR)},
10681072
{"outputs", UniValueType()}, // will be checked by AddOutputs()
1073+
{"reduce_output", UniValueType(UniValue::VNUM)},
10691074
},
10701075
true, true);
10711076

@@ -1080,6 +1085,10 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
10801085
}
10811086
SetFeeEstimateMode(*pwallet, coin_control, conf_target, options["estimate_mode"], options["fee_rate"], /*override_min_fee=*/false);
10821087

1088+
if (options.exists("outputs") && options.exists("reduce_output")) {
1089+
throw JSONRPCError(RPC_INVALID_PARAMETER, "outputs and reduce_output cannot both be specified simultaneously");
1090+
}
1091+
10831092
// Prepare new outputs by creating a temporary tx and calling AddOutputs().
10841093
if (!options["outputs"].isNull()) {
10851094
if (options["outputs"].isArray() && options["outputs"].empty()) {
@@ -1089,6 +1098,10 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
10891098
AddOutputs(tempTx, options["outputs"]);
10901099
outputs = tempTx.vout;
10911100
}
1101+
1102+
if (options.exists("reduce_output")) {
1103+
reduce_output = options["reduce_output"].getInt<uint32_t>();
1104+
}
10921105
}
10931106

10941107
// Make sure the results are valid at least up to the most recent block
@@ -1106,7 +1119,7 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
11061119
CMutableTransaction mtx;
11071120
feebumper::Result res;
11081121
// Targeting feerate bump.
1109-
res = feebumper::CreateRateBumpTransaction(*pwallet, hash, coin_control, errors, old_fee, new_fee, mtx, /*require_mine=*/ !want_psbt, outputs);
1122+
res = feebumper::CreateRateBumpTransaction(*pwallet, hash, coin_control, errors, old_fee, new_fee, mtx, /*require_mine=*/ !want_psbt, outputs, reduce_output);
11101123
if (res != feebumper::Result::OK) {
11111124
switch(res) {
11121125
case feebumper::Result::INVALID_ADDRESS_OR_KEY:

0 commit comments

Comments
 (0)