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.
This commit is contained in:
Andrew Chow 2022-11-07 14:52:56 -05:00
parent a36134fcc7
commit 7d83502d3d
4 changed files with 36 additions and 9 deletions

View File

@ -259,11 +259,13 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "bumpfee", 1, "fee_rate"}, { "bumpfee", 1, "fee_rate"},
{ "bumpfee", 1, "replaceable"}, { "bumpfee", 1, "replaceable"},
{ "bumpfee", 1, "outputs"}, { "bumpfee", 1, "outputs"},
{ "bumpfee", 1, "reduce_output"},
{ "psbtbumpfee", 1, "options" }, { "psbtbumpfee", 1, "options" },
{ "psbtbumpfee", 1, "conf_target"}, { "psbtbumpfee", 1, "conf_target"},
{ "psbtbumpfee", 1, "fee_rate"}, { "psbtbumpfee", 1, "fee_rate"},
{ "psbtbumpfee", 1, "replaceable"}, { "psbtbumpfee", 1, "replaceable"},
{ "psbtbumpfee", 1, "outputs"}, { "psbtbumpfee", 1, "outputs"},
{ "psbtbumpfee", 1, "reduce_output"},
{ "logging", 0, "include" }, { "logging", 0, "include" },
{ "logging", 1, "exclude" }, { "logging", 1, "exclude" },
{ "disconnectnode", 1, "nodeid" }, { "disconnectnode", 1, "nodeid" },

View File

@ -152,8 +152,14 @@ bool TransactionCanBeBumped(const CWallet& wallet, const uint256& txid)
} }
Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCoinControl& coin_control, std::vector<bilingual_str>& errors, Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCoinControl& coin_control, std::vector<bilingual_str>& errors,
CAmount& old_fee, CAmount& new_fee, CMutableTransaction& mtx, bool require_mine, const std::vector<CTxOut>& outputs) CAmount& old_fee, CAmount& new_fee, CMutableTransaction& mtx, bool require_mine, const std::vector<CTxOut>& outputs, std::optional<uint32_t> reduce_output)
{ {
// Cannot both specify new outputs and an output to reduce
if (!outputs.empty() && reduce_output.has_value()) {
errors.push_back(Untranslated("Cannot specify both new outputs to use and an output index to reduce"));
return Result::INVALID_PARAMETER;
}
// We are going to modify coin control later, copy to re-use // We are going to modify coin control later, copy to re-use
CCoinControl new_coin_control(coin_control); CCoinControl new_coin_control(coin_control);
@ -166,6 +172,12 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo
} }
const CWalletTx& wtx = it->second; const CWalletTx& wtx = it->second;
// Make sure that reduce_output is valid
if (reduce_output.has_value() && reduce_output.value() >= wtx.tx->vout.size()) {
errors.push_back(Untranslated("Change position is out of range"));
return Result::INVALID_PARAMETER;
}
// Retrieve all of the UTXOs and add them to coin control // Retrieve all of the UTXOs and add them to coin control
// While we're here, calculate the input amount // While we're here, calculate the input amount
std::map<COutPoint, Coin> coins; std::map<COutPoint, Coin> coins;
@ -233,14 +245,15 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo
std::vector<CRecipient> recipients; std::vector<CRecipient> recipients;
CAmount new_outputs_value = 0; CAmount new_outputs_value = 0;
const auto& txouts = outputs.empty() ? wtx.tx->vout : outputs; const auto& txouts = outputs.empty() ? wtx.tx->vout : outputs;
for (const auto& output : txouts) { for (size_t i = 0; i < txouts.size(); ++i) {
if (!OutputIsChange(wallet, output)) { const CTxOut& output = txouts.at(i);
CRecipient recipient = {output.scriptPubKey, output.nValue, false}; if (reduce_output.has_value() ? reduce_output.value() == i : OutputIsChange(wallet, output)) {
recipients.push_back(recipient);
} else {
CTxDestination change_dest; CTxDestination change_dest;
ExtractDestination(output.scriptPubKey, change_dest); ExtractDestination(output.scriptPubKey, change_dest);
new_coin_control.destChange = change_dest; new_coin_control.destChange = change_dest;
} else {
CRecipient recipient = {output.scriptPubKey, output.nValue, false};
recipients.push_back(recipient);
} }
new_outputs_value += output.nValue; new_outputs_value += output.nValue;
} }

View File

@ -43,6 +43,8 @@ bool TransactionCanBeBumped(const CWallet& wallet, const uint256& txid);
* @param[out] new_fee the fee that the bump transaction pays * @param[out] new_fee the fee that the bump transaction pays
* @param[out] mtx The bump transaction itself * @param[out] mtx The bump transaction itself
* @param[in] require_mine Whether the original transaction must consist of inputs that can be spent by the wallet * @param[in] require_mine Whether the original transaction must consist of inputs that can be spent by the wallet
* @param[in] outputs Vector of new outputs to replace the bumped transaction's outputs
* @param[in] reduce_output The position of the change output to deduct the fee from in the transaction being bumped
*/ */
Result CreateRateBumpTransaction(CWallet& wallet, Result CreateRateBumpTransaction(CWallet& wallet,
const uint256& txid, const uint256& txid,
@ -52,7 +54,8 @@ Result CreateRateBumpTransaction(CWallet& wallet,
CAmount& new_fee, CAmount& new_fee,
CMutableTransaction& mtx, CMutableTransaction& mtx,
bool require_mine, bool require_mine,
const std::vector<CTxOut>& outputs); const std::vector<CTxOut>& outputs,
std::optional<uint32_t> reduce_output = std::nullopt);
//! Sign the new transaction, //! Sign the new transaction,
//! @return false if the tx couldn't be found or if it was //! @return false if the tx couldn't be found or if it was

View File

@ -1015,9 +1015,11 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
"\"" + FeeModes("\"\n\"") + "\""}, "\"" + FeeModes("\"\n\"") + "\""},
{"outputs", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "New outputs (key-value pairs) which will replace\n" {"outputs", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "New outputs (key-value pairs) which will replace\n"
"the original ones, if provided. Each address can only appear once and there can\n" "the original ones, if provided. Each address can only appear once and there can\n"
"only be one \"data\" object.\n", "only be one \"data\" object.\n"
"Cannot be provided if 'reduce_output' is specified.",
OutputsDoc(), OutputsDoc(),
RPCArgOptions{.skip_type_check = true}}, RPCArgOptions{.skip_type_check = true}},
{"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."},
}, },
RPCArgOptions{.oneline_description="options"}}, RPCArgOptions{.oneline_description="options"}},
}, },
@ -1056,6 +1058,8 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
coin_control.m_signal_bip125_rbf = true; coin_control.m_signal_bip125_rbf = true;
std::vector<CTxOut> outputs; std::vector<CTxOut> outputs;
std::optional<uint32_t> reduce_output;
if (!request.params[1].isNull()) { if (!request.params[1].isNull()) {
UniValue options = request.params[1]; UniValue options = request.params[1];
RPCTypeCheckObj(options, RPCTypeCheckObj(options,
@ -1066,6 +1070,7 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
{"replaceable", UniValueType(UniValue::VBOOL)}, {"replaceable", UniValueType(UniValue::VBOOL)},
{"estimate_mode", UniValueType(UniValue::VSTR)}, {"estimate_mode", UniValueType(UniValue::VSTR)},
{"outputs", UniValueType()}, // will be checked by AddOutputs() {"outputs", UniValueType()}, // will be checked by AddOutputs()
{"reduce_output", UniValueType(UniValue::VNUM)},
}, },
true, true); true, true);
@ -1089,6 +1094,10 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
AddOutputs(tempTx, options["outputs"]); AddOutputs(tempTx, options["outputs"]);
outputs = tempTx.vout; outputs = tempTx.vout;
} }
if (options.exists("reduce_output")) {
reduce_output = options["reduce_output"].getInt<uint32_t>();
}
} }
// Make sure the results are valid at least up to the most recent block // Make sure the results are valid at least up to the most recent block
@ -1106,7 +1115,7 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
CMutableTransaction mtx; CMutableTransaction mtx;
feebumper::Result res; feebumper::Result res;
// Targeting feerate bump. // Targeting feerate bump.
res = feebumper::CreateRateBumpTransaction(*pwallet, hash, coin_control, errors, old_fee, new_fee, mtx, /*require_mine=*/ !want_psbt, outputs); res = feebumper::CreateRateBumpTransaction(*pwallet, hash, coin_control, errors, old_fee, new_fee, mtx, /*require_mine=*/ !want_psbt, outputs, reduce_output);
if (res != feebumper::Result::OK) { if (res != feebumper::Result::OK) {
switch(res) { switch(res) {
case feebumper::Result::INVALID_ADDRESS_OR_KEY: case feebumper::Result::INVALID_ADDRESS_OR_KEY: