From 7d83502d3d52218e7b0b0634cff2a9aba9cc77ef Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 7 Nov 2022 14:52:56 -0500 Subject: [PATCH] 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. --- src/rpc/client.cpp | 2 ++ src/wallet/feebumper.cpp | 25 +++++++++++++++++++------ src/wallet/feebumper.h | 5 ++++- src/wallet/rpc/spend.cpp | 13 +++++++++++-- 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index edc0fb05d7..a4c5a26a82 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -259,11 +259,13 @@ static const CRPCConvertParam vRPCConvertParams[] = { "bumpfee", 1, "fee_rate"}, { "bumpfee", 1, "replaceable"}, { "bumpfee", 1, "outputs"}, + { "bumpfee", 1, "reduce_output"}, { "psbtbumpfee", 1, "options" }, { "psbtbumpfee", 1, "conf_target"}, { "psbtbumpfee", 1, "fee_rate"}, { "psbtbumpfee", 1, "replaceable"}, { "psbtbumpfee", 1, "outputs"}, + { "psbtbumpfee", 1, "reduce_output"}, { "logging", 0, "include" }, { "logging", 1, "exclude" }, { "disconnectnode", 1, "nodeid" }, diff --git a/src/wallet/feebumper.cpp b/src/wallet/feebumper.cpp index 0e1f1f9847..3720d144eb 100644 --- a/src/wallet/feebumper.cpp +++ b/src/wallet/feebumper.cpp @@ -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& errors, - CAmount& old_fee, CAmount& new_fee, CMutableTransaction& mtx, bool require_mine, const std::vector& outputs) + CAmount& old_fee, CAmount& new_fee, CMutableTransaction& mtx, bool require_mine, const std::vector& outputs, std::optional 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 CCoinControl new_coin_control(coin_control); @@ -166,6 +172,12 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo } 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 // While we're here, calculate the input amount std::map coins; @@ -233,14 +245,15 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo std::vector recipients; CAmount new_outputs_value = 0; const auto& txouts = outputs.empty() ? wtx.tx->vout : outputs; - for (const auto& output : txouts) { - if (!OutputIsChange(wallet, output)) { - CRecipient recipient = {output.scriptPubKey, output.nValue, false}; - recipients.push_back(recipient); - } else { + for (size_t i = 0; i < txouts.size(); ++i) { + const CTxOut& output = txouts.at(i); + if (reduce_output.has_value() ? reduce_output.value() == i : OutputIsChange(wallet, output)) { CTxDestination change_dest; ExtractDestination(output.scriptPubKey, 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; } diff --git a/src/wallet/feebumper.h b/src/wallet/feebumper.h index 53cf16e0f1..f00bf15730 100644 --- a/src/wallet/feebumper.h +++ b/src/wallet/feebumper.h @@ -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] 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] 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, const uint256& txid, @@ -52,7 +54,8 @@ Result CreateRateBumpTransaction(CWallet& wallet, CAmount& new_fee, CMutableTransaction& mtx, bool require_mine, - const std::vector& outputs); + const std::vector& outputs, + std::optional reduce_output = std::nullopt); //! Sign the new transaction, //! @return false if the tx couldn't be found or if it was diff --git a/src/wallet/rpc/spend.cpp b/src/wallet/rpc/spend.cpp index feee643f26..b695d4bed3 100644 --- a/src/wallet/rpc/spend.cpp +++ b/src/wallet/rpc/spend.cpp @@ -1015,9 +1015,11 @@ static RPCHelpMan bumpfee_helper(std::string method_name) "\"" + FeeModes("\"\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" - "only be one \"data\" object.\n", + "only be one \"data\" object.\n" + "Cannot be provided if 'reduce_output' is specified.", OutputsDoc(), 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"}}, }, @@ -1056,6 +1058,8 @@ static RPCHelpMan bumpfee_helper(std::string method_name) coin_control.m_signal_bip125_rbf = true; std::vector outputs; + std::optional reduce_output; + if (!request.params[1].isNull()) { UniValue options = request.params[1]; RPCTypeCheckObj(options, @@ -1066,6 +1070,7 @@ static RPCHelpMan bumpfee_helper(std::string method_name) {"replaceable", UniValueType(UniValue::VBOOL)}, {"estimate_mode", UniValueType(UniValue::VSTR)}, {"outputs", UniValueType()}, // will be checked by AddOutputs() + {"reduce_output", UniValueType(UniValue::VNUM)}, }, true, true); @@ -1089,6 +1094,10 @@ static RPCHelpMan bumpfee_helper(std::string method_name) AddOutputs(tempTx, options["outputs"]); outputs = tempTx.vout; } + + if (options.exists("reduce_output")) { + reduce_output = options["reduce_output"].getInt(); + } } // 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; feebumper::Result res; // 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) { switch(res) { case feebumper::Result::INVALID_ADDRESS_OR_KEY: