mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-01-10 11:57:28 -03:00
Merge bitcoin/bitcoin#25344: New outputs
argument for bumpfee
/psbtbumpfee
4c8ecccdcd
test: add tests for `outputs` argument to `bumpfee`/`psbtbumpfee` (Seibart Nedor)c0ebb98382
wallet: add `outputs` arguments to `bumpfee` and `psbtbumpfee` (Seibart Nedor)a804f3cfc0
wallet: extract and reuse RPC argument format definition for outputs (Seibart Nedor) Pull request description: This implements a modification of the proposal in #22007: instead of **adding** outputs to the set of outputs in the original transaction, the outputs given by `outputs` argument **completely replace** the outputs in the original transaction. As noted below, this makes it easier to "cancel" a transaction or to reduce the amounts in the outputs, which is not the case with the original proposal in #22007, but it seems from the discussion in this PR that the **replace** behavior is more desirable than **add** one. ACKs for top commit: achow101: ACK4c8ecccdcd
1440000bytes: Code Review ACK4c8ecccdcd
ishaanam: reACK4c8ecccdcd
Tree-SHA512: 31361f4a9b79c162bda7929583b0a3fd200e09f4c1a5378b12007576d6b14e02e9e4f0bab8aa209f08f75ac25a1f4805ad16ebff4a0334b07ad2378cc0090103
This commit is contained in:
commit
73966f75f6
7 changed files with 112 additions and 56 deletions
|
@ -21,12 +21,8 @@
|
|||
#include <util/strencodings.h>
|
||||
#include <util/translation.h>
|
||||
|
||||
CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, std::optional<bool> rbf)
|
||||
void AddInputs(CMutableTransaction& rawTx, const UniValue& inputs_in, std::optional<bool> rbf)
|
||||
{
|
||||
if (outputs_in.isNull()) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, output argument must be non-null");
|
||||
}
|
||||
|
||||
UniValue inputs;
|
||||
if (inputs_in.isNull()) {
|
||||
inputs = UniValue::VARR;
|
||||
|
@ -34,18 +30,6 @@ CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniVal
|
|||
inputs = inputs_in.get_array();
|
||||
}
|
||||
|
||||
const bool outputs_is_obj = outputs_in.isObject();
|
||||
UniValue outputs = outputs_is_obj ? outputs_in.get_obj() : outputs_in.get_array();
|
||||
|
||||
CMutableTransaction rawTx;
|
||||
|
||||
if (!locktime.isNull()) {
|
||||
int64_t nLockTime = locktime.getInt<int64_t>();
|
||||
if (nLockTime < 0 || nLockTime > LOCKTIME_MAX)
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, locktime out of range");
|
||||
rawTx.nLockTime = nLockTime;
|
||||
}
|
||||
|
||||
for (unsigned int idx = 0; idx < inputs.size(); idx++) {
|
||||
const UniValue& input = inputs[idx];
|
||||
const UniValue& o = input.get_obj();
|
||||
|
@ -84,6 +68,16 @@ CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniVal
|
|||
|
||||
rawTx.vin.push_back(in);
|
||||
}
|
||||
}
|
||||
|
||||
void AddOutputs(CMutableTransaction& rawTx, const UniValue& outputs_in)
|
||||
{
|
||||
if (outputs_in.isNull()) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, output argument must be non-null");
|
||||
}
|
||||
|
||||
const bool outputs_is_obj = outputs_in.isObject();
|
||||
UniValue outputs = outputs_is_obj ? outputs_in.get_obj() : outputs_in.get_array();
|
||||
|
||||
if (!outputs_is_obj) {
|
||||
// Translate array of key-value pairs into dict
|
||||
|
@ -132,6 +126,21 @@ CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniVal
|
|||
rawTx.vout.push_back(out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, std::optional<bool> rbf)
|
||||
{
|
||||
CMutableTransaction rawTx;
|
||||
|
||||
if (!locktime.isNull()) {
|
||||
int64_t nLockTime = locktime.getInt<int64_t>();
|
||||
if (nLockTime < 0 || nLockTime > LOCKTIME_MAX)
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, locktime out of range");
|
||||
rawTx.nLockTime = nLockTime;
|
||||
}
|
||||
|
||||
AddInputs(rawTx, inputs_in, rbf);
|
||||
AddOutputs(rawTx, outputs_in);
|
||||
|
||||
if (rbf.has_value() && rbf.value() && rawTx.vin.size() > 0 && !SignalsOptInRBF(CTransaction(rawTx))) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter combination: Sequence number(s) contradict replaceable option");
|
||||
|
|
|
@ -38,6 +38,13 @@ void SignTransactionResultToJSON(CMutableTransaction& mtx, bool complete, const
|
|||
*/
|
||||
void ParsePrevouts(const UniValue& prevTxsUnival, FillableSigningProvider* keystore, std::map<COutPoint, Coin>& coins);
|
||||
|
||||
|
||||
/** Normalize univalue-represented inputs and add them to the transaction */
|
||||
void AddInputs(CMutableTransaction& rawTx, const UniValue& inputs_in, bool rbf);
|
||||
|
||||
/** Normalize univalue-represented outputs and add them to the transaction */
|
||||
void AddOutputs(CMutableTransaction& rawTx, const UniValue& outputs_in);
|
||||
|
||||
/** Create a transaction from univalue parameters */
|
||||
CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, std::optional<bool> rbf);
|
||||
|
||||
|
|
|
@ -155,7 +155,7 @@ bool TransactionCanBeBumped(const CWallet& wallet, const uint256& txid)
|
|||
}
|
||||
|
||||
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)
|
||||
CAmount& old_fee, CAmount& new_fee, CMutableTransaction& mtx, bool require_mine, const std::vector<CTxOut>& outputs)
|
||||
{
|
||||
// We are going to modify coin control later, copy to re-use
|
||||
CCoinControl new_coin_control(coin_control);
|
||||
|
@ -222,11 +222,19 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo
|
|||
return result;
|
||||
}
|
||||
|
||||
// Fill in recipients(and preserve a single change key if there is one)
|
||||
// While we're here, calculate the output amount
|
||||
std::vector<CRecipient> recipients;
|
||||
// Calculate the old output amount.
|
||||
CAmount output_value = 0;
|
||||
for (const auto& output : wtx.tx->vout) {
|
||||
for (const auto& old_output : wtx.tx->vout) {
|
||||
output_value += old_output.nValue;
|
||||
}
|
||||
|
||||
old_fee = input_value - output_value;
|
||||
|
||||
// Fill in recipients (and preserve a single change key if there
|
||||
// is one). If outputs vector is non-empty, replace original
|
||||
// outputs with its contents, otherwise use original outputs.
|
||||
std::vector<CRecipient> recipients;
|
||||
for (const auto& output : outputs.empty() ? wtx.tx->vout : outputs) {
|
||||
if (!OutputIsChange(wallet, output)) {
|
||||
CRecipient recipient = {output.scriptPubKey, output.nValue, false};
|
||||
recipients.push_back(recipient);
|
||||
|
@ -235,11 +243,8 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo
|
|||
ExtractDestination(output.scriptPubKey, change_dest);
|
||||
new_coin_control.destChange = change_dest;
|
||||
}
|
||||
output_value += output.nValue;
|
||||
}
|
||||
|
||||
old_fee = input_value - output_value;
|
||||
|
||||
if (coin_control.m_feerate) {
|
||||
// The user provided a feeRate argument.
|
||||
// We calculate this here to avoid compiler warning on the cs_wallet lock
|
||||
|
|
|
@ -51,7 +51,8 @@ Result CreateRateBumpTransaction(CWallet& wallet,
|
|||
CAmount& old_fee,
|
||||
CAmount& new_fee,
|
||||
CMutableTransaction& mtx,
|
||||
bool require_mine);
|
||||
bool require_mine,
|
||||
const std::vector<CTxOut>& outputs);
|
||||
|
||||
//! Sign the new transaction,
|
||||
//! @return false if the tx couldn't be found or if it was
|
||||
|
|
|
@ -291,7 +291,8 @@ public:
|
|||
CAmount& new_fee,
|
||||
CMutableTransaction& mtx) override
|
||||
{
|
||||
return feebumper::CreateRateBumpTransaction(*m_wallet.get(), txid, coin_control, errors, old_fee, new_fee, mtx, /* require_mine= */ true) == feebumper::Result::OK;
|
||||
std::vector<CTxOut> outputs; // just an empty list of new recipients for now
|
||||
return feebumper::CreateRateBumpTransaction(*m_wallet.get(), txid, coin_control, errors, old_fee, new_fee, mtx, /* require_mine= */ true, outputs) == feebumper::Result::OK;
|
||||
}
|
||||
bool signBumpTransaction(CMutableTransaction& mtx) override { return feebumper::SignTransaction(*m_wallet.get(), mtx); }
|
||||
bool commitBumpTransaction(const uint256& txid,
|
||||
|
|
|
@ -956,6 +956,26 @@ RPCHelpMan signrawtransactionwithwallet()
|
|||
};
|
||||
}
|
||||
|
||||
// Definition of allowed formats of specifying transaction outputs in
|
||||
// `bumpfee`, `psbtbumpfee`, `send` and `walletcreatefundedpsbt` RPCs.
|
||||
static std::vector<RPCArg> OutputsDoc()
|
||||
{
|
||||
return
|
||||
{
|
||||
{"", RPCArg::Type::OBJ_USER_KEYS, RPCArg::Optional::OMITTED, "",
|
||||
{
|
||||
{"address", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "A key-value pair. The key (string) is the bitcoin address,\n"
|
||||
"the value (float or string) is the amount in " + CURRENCY_UNIT + ""},
|
||||
},
|
||||
},
|
||||
{"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "",
|
||||
{
|
||||
{"data", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "A key-value pair. The key must be \"data\", the value is hex-encoded data"},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static RPCHelpMan bumpfee_helper(std::string method_name)
|
||||
{
|
||||
const bool want_psbt = method_name == "psbtbumpfee";
|
||||
|
@ -992,7 +1012,12 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
|
|||
"still be replaceable in practice, for example if it has unconfirmed ancestors which\n"
|
||||
"are replaceable).\n"},
|
||||
{"estimate_mode", RPCArg::Type::STR, RPCArg::Default{"unset"}, "The fee estimate mode, must be one of (case insensitive):\n"
|
||||
"\"" + FeeModes("\"\n\"") + "\""},
|
||||
"\"" + 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",
|
||||
OutputsDoc(),
|
||||
RPCArgOptions{.skip_type_check = true}},
|
||||
},
|
||||
RPCArgOptions{.oneline_description="options"}},
|
||||
},
|
||||
|
@ -1029,6 +1054,7 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
|
|||
coin_control.fAllowWatchOnly = pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS);
|
||||
// optional parameters
|
||||
coin_control.m_signal_bip125_rbf = true;
|
||||
std::vector<CTxOut> outputs;
|
||||
|
||||
if (!request.params[1].isNull()) {
|
||||
UniValue options = request.params[1];
|
||||
|
@ -1039,6 +1065,7 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
|
|||
{"fee_rate", UniValueType()}, // will be checked by AmountFromValue() in SetFeeEstimateMode()
|
||||
{"replaceable", UniValueType(UniValue::VBOOL)},
|
||||
{"estimate_mode", UniValueType(UniValue::VSTR)},
|
||||
{"outputs", UniValueType()}, // will be checked by AddOutputs()
|
||||
},
|
||||
true, true);
|
||||
|
||||
|
@ -1052,6 +1079,16 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
|
|||
coin_control.m_signal_bip125_rbf = options["replaceable"].get_bool();
|
||||
}
|
||||
SetFeeEstimateMode(*pwallet, coin_control, conf_target, options["estimate_mode"], options["fee_rate"], /*override_min_fee=*/false);
|
||||
|
||||
// Prepare new outputs by creating a temporary tx and calling AddOutputs().
|
||||
if (!options["outputs"].isNull()) {
|
||||
if (options["outputs"].isArray() && options["outputs"].empty()) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, output argument cannot be an empty array");
|
||||
}
|
||||
CMutableTransaction tempTx;
|
||||
AddOutputs(tempTx, options["outputs"]);
|
||||
outputs = tempTx.vout;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the results are valid at least up to the most recent block
|
||||
|
@ -1069,7 +1106,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);
|
||||
res = feebumper::CreateRateBumpTransaction(*pwallet, hash, coin_control, errors, old_fee, new_fee, mtx, /*require_mine=*/ !want_psbt, outputs);
|
||||
if (res != feebumper::Result::OK) {
|
||||
switch(res) {
|
||||
case feebumper::Result::INVALID_ADDRESS_OR_KEY:
|
||||
|
@ -1144,18 +1181,7 @@ RPCHelpMan send()
|
|||
{"outputs", RPCArg::Type::ARR, RPCArg::Optional::NO, "The outputs (key-value pairs), where none of the keys are duplicated.\n"
|
||||
"That is, each address can only appear once and there can only be one 'data' object.\n"
|
||||
"For convenience, a dictionary, which holds the key-value pairs directly, is also accepted.",
|
||||
{
|
||||
{"", RPCArg::Type::OBJ_USER_KEYS, RPCArg::Optional::OMITTED, "",
|
||||
{
|
||||
{"address", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "A key-value pair. The key (string) is the bitcoin address, the value (float or string) is the amount in " + CURRENCY_UNIT + ""},
|
||||
},
|
||||
},
|
||||
{"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "",
|
||||
{
|
||||
{"data", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "A key-value pair. The key must be \"data\", the value is hex-encoded data"},
|
||||
},
|
||||
},
|
||||
},
|
||||
OutputsDoc(),
|
||||
RPCArgOptions{.skip_type_check = true}},
|
||||
{"conf_target", RPCArg::Type::NUM, RPCArg::DefaultHint{"wallet -txconfirmtarget"}, "Confirmation target in blocks"},
|
||||
{"estimate_mode", RPCArg::Type::STR, RPCArg::Default{"unset"}, "The fee estimate mode, must be one of (case insensitive):\n"
|
||||
|
@ -1606,19 +1632,8 @@ RPCHelpMan walletcreatefundedpsbt()
|
|||
"That is, each address can only appear once and there can only be one 'data' object.\n"
|
||||
"For compatibility reasons, a dictionary, which holds the key-value pairs directly, is also\n"
|
||||
"accepted as second parameter.",
|
||||
{
|
||||
{"", RPCArg::Type::OBJ_USER_KEYS, RPCArg::Optional::OMITTED, "",
|
||||
{
|
||||
{"address", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "A key-value pair. The key (string) is the bitcoin address, the value (float or string) is the amount in " + CURRENCY_UNIT + ""},
|
||||
},
|
||||
},
|
||||
{"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "",
|
||||
{
|
||||
{"data", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "A key-value pair. The key must be \"data\", the value is hex-encoded data"},
|
||||
},
|
||||
},
|
||||
},
|
||||
RPCArgOptions{.skip_type_check = true}},
|
||||
OutputsDoc(),
|
||||
RPCArgOptions{.skip_type_check = true}},
|
||||
{"locktime", RPCArg::Type::NUM, RPCArg::Default{0}, "Raw locktime. Non-0 value also locktime-activates inputs"},
|
||||
{"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "",
|
||||
Cat<std::vector<RPCArg>>(
|
||||
|
|
|
@ -81,7 +81,7 @@ class BumpFeeTest(BitcoinTestFramework):
|
|||
|
||||
self.log.info("Running tests")
|
||||
dest_address = peer_node.getnewaddress()
|
||||
for mode in ["default", "fee_rate"]:
|
||||
for mode in ["default", "fee_rate", "new_outputs"]:
|
||||
test_simple_bumpfee_succeeds(self, mode, rbf_node, peer_node, dest_address)
|
||||
self.test_invalid_parameters(rbf_node, peer_node, dest_address)
|
||||
test_segwit_bumpfee_succeeds(self, rbf_node, dest_address)
|
||||
|
@ -157,6 +157,14 @@ class BumpFeeTest(BitcoinTestFramework):
|
|||
assert_raises_rpc_error(-8, 'Invalid estimate_mode parameter, must be one of: "unset", "economical", "conservative"',
|
||||
rbf_node.bumpfee, rbfid, {"estimate_mode": mode})
|
||||
|
||||
self.log.info("Test invalid outputs values")
|
||||
assert_raises_rpc_error(-8, "Invalid parameter, output argument cannot be an empty array",
|
||||
rbf_node.bumpfee, rbfid, {"outputs": []})
|
||||
assert_raises_rpc_error(-8, "Invalid parameter, duplicated address: " + dest_address,
|
||||
rbf_node.bumpfee, rbfid, {"outputs": [{dest_address: 0.1}, {dest_address: 0.2}]})
|
||||
assert_raises_rpc_error(-8, "Invalid parameter, duplicate key: data",
|
||||
rbf_node.bumpfee, rbfid, {"outputs": [{"data": "deadbeef"}, {"data": "deadbeef"}]})
|
||||
|
||||
self.clear_mempool()
|
||||
|
||||
|
||||
|
@ -169,6 +177,10 @@ def test_simple_bumpfee_succeeds(self, mode, rbf_node, peer_node, dest_address):
|
|||
if mode == "fee_rate":
|
||||
bumped_psbt = rbf_node.psbtbumpfee(rbfid, {"fee_rate": str(NORMAL)})
|
||||
bumped_tx = rbf_node.bumpfee(rbfid, {"fee_rate": NORMAL})
|
||||
elif mode == "new_outputs":
|
||||
new_address = peer_node.getnewaddress()
|
||||
bumped_psbt = rbf_node.psbtbumpfee(rbfid, {"outputs": {new_address: 0.0003}})
|
||||
bumped_tx = rbf_node.bumpfee(rbfid, {"outputs": {new_address: 0.0003}})
|
||||
else:
|
||||
bumped_psbt = rbf_node.psbtbumpfee(rbfid)
|
||||
bumped_tx = rbf_node.bumpfee(rbfid)
|
||||
|
@ -192,6 +204,10 @@ def test_simple_bumpfee_succeeds(self, mode, rbf_node, peer_node, dest_address):
|
|||
bumpedwtx = rbf_node.gettransaction(bumped_tx["txid"])
|
||||
assert_equal(oldwtx["replaced_by_txid"], bumped_tx["txid"])
|
||||
assert_equal(bumpedwtx["replaces_txid"], rbfid)
|
||||
# if this is a new_outputs test, check that outputs were indeed replaced
|
||||
if mode == "new_outputs":
|
||||
assert len(bumpedwtx["details"]) == 1
|
||||
assert bumpedwtx["details"][0]["address"] == new_address
|
||||
self.clear_mempool()
|
||||
|
||||
|
||||
|
@ -628,12 +644,14 @@ def test_change_script_match(self, rbf_node, dest_address):
|
|||
self.clear_mempool()
|
||||
|
||||
|
||||
def spend_one_input(node, dest_address, change_size=Decimal("0.00049000")):
|
||||
def spend_one_input(node, dest_address, change_size=Decimal("0.00049000"), data=None):
|
||||
tx_input = dict(
|
||||
sequence=MAX_BIP125_RBF_SEQUENCE, **next(u for u in node.listunspent() if u["amount"] == Decimal("0.00100000")))
|
||||
destinations = {dest_address: Decimal("0.00050000")}
|
||||
if change_size > 0:
|
||||
destinations[node.getrawchangeaddress()] = change_size
|
||||
if data:
|
||||
destinations['data'] = data
|
||||
rawtx = node.createrawtransaction([tx_input], destinations)
|
||||
signedtx = node.signrawtransactionwithwallet(rawtx)
|
||||
txid = node.sendrawtransaction(signedtx["hex"])
|
||||
|
|
Loading…
Reference in a new issue