From 052427eef1c9da84c474c5161b1910d3328ef0da Mon Sep 17 00:00:00 2001 From: Jon Atack Date: Thu, 22 Oct 2020 15:50:03 +0200 Subject: [PATCH 01/11] wallet, bugfix: fix bumpfee with explicit fee rate modes --- src/wallet/rpcwallet.cpp | 1 - test/functional/wallet_bumpfee.py | 15 ++++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 2295fb0ef1..46e27bb4bf 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -3461,7 +3461,6 @@ static RPCHelpMan bumpfee_helper(std::string method_name) if (options.exists("fee_rate")) { throw JSONRPCError(RPC_INVALID_PARAMETER, "conf_target can't be set with fee_rate. Please provide either a confirmation target in blocks for automatic fee estimation, or an explicit fee rate."); } - coin_control.m_confirm_target = ParseConfirmTarget(conf_target, pwallet->chain().estimateMaxBlocks()); } else if (options.exists("fee_rate")) { CFeeRate fee_rate(AmountFromValue(options["fee_rate"])); if (fee_rate <= CFeeRate(0)) { diff --git a/test/functional/wallet_bumpfee.py b/test/functional/wallet_bumpfee.py index 4b29e65b09..7dac2d7eaf 100755 --- a/test/functional/wallet_bumpfee.py +++ b/test/functional/wallet_bumpfee.py @@ -17,7 +17,7 @@ import io from test_framework.blocktools import add_witness_commitment, create_block, create_coinbase, send_to_witness -from test_framework.messages import BIP125_SEQUENCE_NUMBER, CTransaction +from test_framework.messages import BIP125_SEQUENCE_NUMBER, COIN, CTransaction from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, @@ -36,6 +36,8 @@ HIGH = 0.00500000 TOO_HIGH = 1.00000000 +BTC_MODE = "BTC/kB" +SAT_MODE = "sat/B" class BumpFeeTest(BitcoinTestFramework): def set_test_params(self): @@ -77,8 +79,8 @@ def run_test(self): self.log.info("Running tests") dest_address = peer_node.getnewaddress() self.test_invalid_parameters(rbf_node, dest_address) - test_simple_bumpfee_succeeds(self, "default", rbf_node, peer_node, dest_address) - test_simple_bumpfee_succeeds(self, "fee_rate", rbf_node, peer_node, dest_address) + for mode in ["default", "fee_rate", BTC_MODE, SAT_MODE]: + test_simple_bumpfee_succeeds(self, mode, rbf_node, peer_node, dest_address) test_feerate_args(self, rbf_node, peer_node, dest_address) test_segwit_bumpfee_succeeds(self, rbf_node, dest_address) test_nonrbf_bumpfee_fails(self, peer_node, dest_address) @@ -132,6 +134,13 @@ 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": NORMAL}) bumped_tx = rbf_node.bumpfee(rbfid, {"fee_rate": NORMAL}) + elif mode == BTC_MODE: + bumped_psbt = rbf_node.psbtbumpfee(rbfid, {"conf_target": NORMAL, "estimate_mode": BTC_MODE}) + bumped_tx = rbf_node.bumpfee(rbfid, {"conf_target": NORMAL, "estimate_mode": BTC_MODE}) + elif mode == SAT_MODE: + sat_fee = NORMAL * COIN / 1000 # convert NORMAL from BTC/kB to sat/B + bumped_psbt = rbf_node.psbtbumpfee(rbfid, {"conf_target": sat_fee, "estimate_mode": SAT_MODE}) + bumped_tx = rbf_node.bumpfee(rbfid, {"conf_target": sat_fee, "estimate_mode": SAT_MODE}) else: bumped_psbt = rbf_node.psbtbumpfee(rbfid) bumped_tx = rbf_node.bumpfee(rbfid) From fc5721723d34f76f9e1ffd2e31f274ea6b22f894 Mon Sep 17 00:00:00 2001 From: Jon Atack Date: Sat, 24 Oct 2020 19:48:04 +0200 Subject: [PATCH 02/11] wallet: fix SetFeeEstimateMode() error message to clarify for the user the confusing error message that the missing fee rate needs to be set in the conf_target param/option. --- src/wallet/rpcwallet.cpp | 2 +- test/functional/wallet_basic.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 46e27bb4bf..614227fde2 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -214,7 +214,7 @@ static void SetFeeEstimateMode(const CWallet* pwallet, CCoinControl& cc, const U if (cc.m_fee_mode == FeeEstimateMode::BTC_KB || cc.m_fee_mode == FeeEstimateMode::SAT_B) { if (estimate_param.isNull()) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Selected estimate_mode requires a fee rate"); + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Selected estimate_mode %s requires a fee rate to be specified in conf_target", estimate_mode.get_str())); } CAmount fee_rate = AmountFromValue(estimate_param); diff --git a/test/functional/wallet_basic.py b/test/functional/wallet_basic.py index 689a0fa4df..7edcf41bea 100755 --- a/test/functional/wallet_basic.py +++ b/test/functional/wallet_basic.py @@ -228,7 +228,7 @@ def run_test(self): # Sendmany with explicit fee (BTC/kB) # Throw if no conf_target provided - assert_raises_rpc_error(-8, "Selected estimate_mode requires a fee rate", + assert_raises_rpc_error(-8, "Selected estimate_mode bTc/kB requires a fee rate to be specified in conf_target", self.nodes[2].sendmany, amounts={ address: 10 }, estimate_mode='bTc/kB') @@ -254,7 +254,7 @@ def run_test(self): # Sendmany with explicit fee (SAT/B) # Throw if no conf_target provided - assert_raises_rpc_error(-8, "Selected estimate_mode requires a fee rate", + assert_raises_rpc_error(-8, "Selected estimate_mode sat/b requires a fee rate to be specified in conf_target", self.nodes[2].sendmany, amounts={ address: 10 }, estimate_mode='sat/b') @@ -421,7 +421,7 @@ def run_test(self): assert prebalance > 2 address = self.nodes[1].getnewaddress() # Throw if no conf_target provided - assert_raises_rpc_error(-8, "Selected estimate_mode requires a fee rate", + assert_raises_rpc_error(-8, "Selected estimate_mode BTc/Kb requires a fee rate to be specified in conf_target", self.nodes[2].sendtoaddress, address=address, amount=1.0, @@ -455,7 +455,7 @@ def run_test(self): assert prebalance > 2 address = self.nodes[1].getnewaddress() # Throw if no conf_target provided - assert_raises_rpc_error(-8, "Selected estimate_mode requires a fee rate", + assert_raises_rpc_error(-8, "Selected estimate_mode SAT/b requires a fee rate to be specified in conf_target", self.nodes[2].sendtoaddress, address=address, amount=1.0, From 1697a40b6f841a54ee0d9744ed7fd09034b0ddad Mon Sep 17 00:00:00 2001 From: Jon Atack Date: Sat, 27 Jun 2020 05:53:17 +0200 Subject: [PATCH 03/11] wallet: improve bumpfee error/help, add explicit fee rate coverage --- src/wallet/rpcwallet.cpp | 8 +++--- test/functional/wallet_bumpfee.py | 47 +++++++++++++++++++++++++------ 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 614227fde2..9bb09dda16 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -3039,7 +3039,7 @@ static RPCHelpMan listunspent() }; } -void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& fee_out, int& change_position, UniValue options, CCoinControl& coinControl) +void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& fee_out, int& change_position, const UniValue& options, CCoinControl& coinControl) { // Make sure the results are valid at least up to the most recent block // the user could have gotten from another RPC command prior to now @@ -3373,7 +3373,7 @@ static RPCHelpMan bumpfee_helper(std::string method_name) "The command will pay the additional fee by reducing change outputs or adding inputs when necessary. It may add a new change output if one does not already exist.\n" "All inputs in the original transaction will be included in the replacement transaction.\n" "The command will fail if the wallet or mempool contains a transaction that spends one of T's outputs.\n" - "By default, the new fee will be calculated automatically using estimatesmartfee.\n" + "By default, the new fee will be calculated automatically using the estimatesmartfee RPC.\n" "The user can specify a confirmation target for estimatesmartfee.\n" "Alternatively, the user can specify a fee_rate (" + CURRENCY_UNIT + " per kB) for the new transaction.\n" "At a minimum, the new fee rate must be high enough to pay an additional new relay fee (incrementalfee\n" @@ -3459,7 +3459,7 @@ static RPCHelpMan bumpfee_helper(std::string method_name) if (!conf_target.isNull()) { if (options.exists("fee_rate")) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "conf_target can't be set with fee_rate. Please provide either a confirmation target in blocks for automatic fee estimation, or an explicit fee rate."); + throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot specify both conf_target and fee_rate. Please provide either a confirmation target in blocks for automatic fee estimation, or an explicit fee rate."); } } else if (options.exists("fee_rate")) { CFeeRate fee_rate(AmountFromValue(options["fee_rate"])); @@ -4115,7 +4115,7 @@ static RPCHelpMan send() CMutableTransaction rawTx = ConstructTransaction(options["inputs"], request.params[0], options["locktime"], rbf); CCoinControl coin_control; // Automatically select coins, unless at least one is manually selected. Can - // be overriden by options.add_inputs. + // be overridden by options.add_inputs. coin_control.m_add_inputs = rawTx.vin.size() == 0; FundTransaction(pwallet, rawTx, fee, change_position, options, coin_control); diff --git a/test/functional/wallet_bumpfee.py b/test/functional/wallet_bumpfee.py index 7dac2d7eaf..e3766c673e 100755 --- a/test/functional/wallet_bumpfee.py +++ b/test/functional/wallet_bumpfee.py @@ -173,19 +173,50 @@ def test_feerate_args(self, rbf_node, peer_node, dest_address): self.sync_mempools((rbf_node, peer_node)) assert rbfid in rbf_node.getrawmempool() and rbfid in peer_node.getrawmempool() - assert_raises_rpc_error(-8, "conf_target can't be set with fee_rate. Please provide either a confirmation target in blocks for automatic fee estimation, or an explicit fee rate.", rbf_node.bumpfee, rbfid, {"fee_rate": NORMAL, "confTarget": 1}) - assert_raises_rpc_error(-3, "Unexpected key totalFee", rbf_node.bumpfee, rbfid, {"totalFee": NORMAL}) - assert_raises_rpc_error(-8, "conf_target can't be set with fee_rate. Please provide either a confirmation target in blocks for automatic fee estimation, or an explicit fee rate.", rbf_node.bumpfee, rbfid, {"fee_rate":0.00001, "confTarget": 1}) - - # Bumping to just above minrelay should fail to increase total fee enough, at least - assert_raises_rpc_error(-8, "Insufficient total fee", rbf_node.bumpfee, rbfid, {"fee_rate": INSUFFICIENT}) + # For each fee mode, bumping to just above minrelay should fail to increase the total fee enough. + for options in [{"fee_rate": INSUFFICIENT}, {"conf_target": INSUFFICIENT, "estimate_mode": BTC_MODE}, {"conf_target": 1, "estimate_mode": SAT_MODE}]: + assert_raises_rpc_error(-8, "Insufficient total fee", rbf_node.bumpfee, rbfid, options) assert_raises_rpc_error(-3, "Amount out of range", rbf_node.bumpfee, rbfid, {"fee_rate": -1}) - assert_raises_rpc_error(-4, "is too high (cannot be higher than", rbf_node.bumpfee, rbfid, {"fee_rate": TOO_HIGH}) - self.clear_mempool() + self.log.info("Test explicit fee rate raises RPC error if estimate_mode is passed without a conf_target") + for unit, fee_rate in {"SAT/B": 10, "BTC/KB": NORMAL}.items(): + assert_raises_rpc_error(-8, "Selected estimate_mode {} requires a fee rate to be specified in conf_target".format(unit), + rbf_node.bumpfee, rbfid, {"fee_rate": fee_rate, "estimate_mode": unit}) + + self.log.info("Test explicit fee rate raises RPC error if both fee_rate and conf_target are passed") + error_both = "Cannot specify both conf_target and fee_rate. Please provide either a confirmation " \ + "target in blocks for automatic fee estimation, or an explicit fee rate." + assert_raises_rpc_error(-8, error_both, rbf_node.bumpfee, rbfid, {"conf_target": NORMAL, "fee_rate": NORMAL}) + + self.log.info("Test invalid conf_target settings") + for field in ["confTarget", "conf_target"]: + assert_raises_rpc_error(-8, error_both, rbf_node.bumpfee, rbfid, {field: 1, "fee_rate": NORMAL}) + too_high = "is too high (cannot be higher than -maxtxfee" + assert_raises_rpc_error(-4, too_high, lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": BTC_MODE, "conf_target": 2009})) + assert_raises_rpc_error(-4, too_high, lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": SAT_MODE, "conf_target": 2009 * 10000})) + + self.log.info("Test invalid estimate_mode settings") + for k, v in {"number": 42, "object": {"foo": "bar"}}.items(): + assert_raises_rpc_error(-3, "Expected type string for estimate_mode, got {}".format(k), + lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": v, "fee_rate": NORMAL})) + for mode in ["foo", Decimal("3.141592")]: + assert_raises_rpc_error(-8, "Invalid estimate_mode parameter", + lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": mode, "fee_rate": NORMAL})) + + self.log.info("Test invalid fee_rate settings") + for mode in ["unset", "economical", "conservative", BTC_MODE, SAT_MODE]: + self.log.debug("{}".format(mode)) + for k, v in {"string": "", "object": {"foo": "bar"}}.items(): + assert_raises_rpc_error(-3, "Expected type number for fee_rate, got {}".format(k), + lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": mode, "fee_rate": v})) + assert_raises_rpc_error(-3, "Amount out of range", + lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": mode, "fee_rate": -1})) + assert_raises_rpc_error(-8, "Invalid fee_rate 0.00000000 BTC/kB (must be greater than 0)", + lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": mode, "fee_rate": 0})) + self.clear_mempool() def test_segwit_bumpfee_succeeds(self, rbf_node, dest_address): self.log.info('Test that segwit-sourcing bumpfee works') From 2d8eba8f8425a2515022d51f1f5b4911329fbf55 Mon Sep 17 00:00:00 2001 From: Jon Atack Date: Thu, 22 Oct 2020 17:01:10 +0200 Subject: [PATCH 04/11] wallet: combine redundant bumpfee invalid params and args tests might be best reviewed with: git show COMMIT_HASH -w --color-moved=dimmed-zebra --- test/functional/wallet_bumpfee.py | 126 ++++++++++++------------------ 1 file changed, 51 insertions(+), 75 deletions(-) diff --git a/test/functional/wallet_bumpfee.py b/test/functional/wallet_bumpfee.py index e3766c673e..20d243699f 100755 --- a/test/functional/wallet_bumpfee.py +++ b/test/functional/wallet_bumpfee.py @@ -78,10 +78,9 @@ def run_test(self): self.log.info("Running tests") dest_address = peer_node.getnewaddress() - self.test_invalid_parameters(rbf_node, dest_address) for mode in ["default", "fee_rate", BTC_MODE, SAT_MODE]: test_simple_bumpfee_succeeds(self, mode, rbf_node, peer_node, dest_address) - test_feerate_args(self, 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) test_nonrbf_bumpfee_fails(self, peer_node, dest_address) test_notmine_bumpfee_fails(self, rbf_node, peer_node, dest_address) @@ -100,28 +99,56 @@ def run_test(self): test_small_output_with_feerate_succeeds(self, rbf_node, dest_address) test_no_more_inputs_fails(self, rbf_node, dest_address) - def test_invalid_parameters(self, node, dest_address): - txid = spend_one_input(node, dest_address) - # invalid estimate mode - assert_raises_rpc_error(-8, "Invalid estimate_mode parameter", node.bumpfee, txid, { - "estimate_mode": "moo", - }) - assert_raises_rpc_error(-3, "Expected type string", node.bumpfee, txid, { - "estimate_mode": 38, - }) - assert_raises_rpc_error(-3, "Expected type string", node.bumpfee, txid, { - "estimate_mode": { - "foo": "bar", - }, - }) - assert_raises_rpc_error(-8, "Invalid estimate_mode parameter", node.bumpfee, txid, { - "estimate_mode": Decimal("3.141592"), - }) - # confTarget and conf_target - assert_raises_rpc_error(-8, "confTarget and conf_target options should not both be set", node.bumpfee, txid, { - "confTarget": 123, - "conf_target": 456, - }) + def test_invalid_parameters(self, rbf_node, peer_node, dest_address): + self.log.info('Test invalid parameters') + rbfid = spend_one_input(rbf_node, dest_address) + self.sync_mempools((rbf_node, peer_node)) + assert rbfid in rbf_node.getrawmempool() and rbfid in peer_node.getrawmempool() + + assert_raises_rpc_error(-3, "Unexpected key totalFee", rbf_node.bumpfee, rbfid, {"totalFee": NORMAL}) + assert_raises_rpc_error(-4, "is too high (cannot be higher than", rbf_node.bumpfee, rbfid, {"fee_rate": TOO_HIGH}) + + # For each fee mode, bumping to just above minrelay should fail to increase the total fee enough. + for options in [{"fee_rate": INSUFFICIENT}, {"conf_target": INSUFFICIENT, "estimate_mode": BTC_MODE}, {"conf_target": 1, "estimate_mode": SAT_MODE}]: + assert_raises_rpc_error(-8, "Insufficient total fee", rbf_node.bumpfee, rbfid, options) + + self.log.info("Test explicit fee rate raises RPC error if estimate_mode is passed without a conf_target") + for unit, fee_rate in {"SAT/B": 100, "BTC/KB": NORMAL}.items(): + assert_raises_rpc_error(-8, "Selected estimate_mode {} requires a fee rate to be specified in conf_target".format(unit), + rbf_node.bumpfee, rbfid, {"fee_rate": fee_rate, "estimate_mode": unit}) + + self.log.info("Test explicit fee rate raises RPC error if both fee_rate and conf_target are passed") + error_both = "Cannot specify both conf_target and fee_rate. Please provide either a confirmation " \ + "target in blocks for automatic fee estimation, or an explicit fee rate." + assert_raises_rpc_error(-8, error_both, rbf_node.bumpfee, rbfid, {"conf_target": NORMAL, "fee_rate": NORMAL}) + + self.log.info("Test invalid conf_target settings") + assert_raises_rpc_error(-8, "confTarget and conf_target options should not both be set", + rbf_node.bumpfee, rbfid, {"confTarget": 123, "conf_target": 456}) + for field in ["confTarget", "conf_target"]: + assert_raises_rpc_error(-4, "is too high (cannot be higher than -maxtxfee", + lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": BTC_MODE, "conf_target": 2009})) + assert_raises_rpc_error(-4, "is too high (cannot be higher than -maxtxfee", + lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": SAT_MODE, "conf_target": 2009 * 10000})) + + self.log.info("Test invalid estimate_mode settings") + for k, v in {"number": 42, "object": {"foo": "bar"}}.items(): + assert_raises_rpc_error(-3, "Expected type string for estimate_mode, got {}".format(k), + lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": v, "fee_rate": NORMAL})) + for mode in ["foo", Decimal("3.141592")]: + assert_raises_rpc_error(-8, "Invalid estimate_mode parameter", + lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": mode, "fee_rate": NORMAL})) + + self.log.info("Test invalid fee_rate settings") + for mode in ["unset", "economical", "conservative", BTC_MODE, SAT_MODE]: + self.log.debug("{}".format(mode)) + for k, v in {"string": "", "object": {"foo": "bar"}}.items(): + assert_raises_rpc_error(-3, "Expected type number for fee_rate, got {}".format(k), + lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": mode, "fee_rate": v})) + assert_raises_rpc_error(-3, "Amount out of range", + lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": mode, "fee_rate": -1})) + assert_raises_rpc_error(-8, "Invalid fee_rate 0.00000000 BTC/kB (must be greater than 0)", + lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": mode, "fee_rate": 0})) self.clear_mempool() @@ -167,57 +194,6 @@ def test_simple_bumpfee_succeeds(self, mode, rbf_node, peer_node, dest_address): self.clear_mempool() -def test_feerate_args(self, rbf_node, peer_node, dest_address): - self.log.info('Test fee_rate args') - rbfid = spend_one_input(rbf_node, dest_address) - self.sync_mempools((rbf_node, peer_node)) - assert rbfid in rbf_node.getrawmempool() and rbfid in peer_node.getrawmempool() - - assert_raises_rpc_error(-3, "Unexpected key totalFee", rbf_node.bumpfee, rbfid, {"totalFee": NORMAL}) - - # For each fee mode, bumping to just above minrelay should fail to increase the total fee enough. - for options in [{"fee_rate": INSUFFICIENT}, {"conf_target": INSUFFICIENT, "estimate_mode": BTC_MODE}, {"conf_target": 1, "estimate_mode": SAT_MODE}]: - assert_raises_rpc_error(-8, "Insufficient total fee", rbf_node.bumpfee, rbfid, options) - assert_raises_rpc_error(-3, "Amount out of range", rbf_node.bumpfee, rbfid, {"fee_rate": -1}) - assert_raises_rpc_error(-4, "is too high (cannot be higher than", rbf_node.bumpfee, rbfid, {"fee_rate": TOO_HIGH}) - - self.log.info("Test explicit fee rate raises RPC error if estimate_mode is passed without a conf_target") - for unit, fee_rate in {"SAT/B": 10, "BTC/KB": NORMAL}.items(): - assert_raises_rpc_error(-8, "Selected estimate_mode {} requires a fee rate to be specified in conf_target".format(unit), - rbf_node.bumpfee, rbfid, {"fee_rate": fee_rate, "estimate_mode": unit}) - - self.log.info("Test explicit fee rate raises RPC error if both fee_rate and conf_target are passed") - error_both = "Cannot specify both conf_target and fee_rate. Please provide either a confirmation " \ - "target in blocks for automatic fee estimation, or an explicit fee rate." - assert_raises_rpc_error(-8, error_both, rbf_node.bumpfee, rbfid, {"conf_target": NORMAL, "fee_rate": NORMAL}) - - self.log.info("Test invalid conf_target settings") - for field in ["confTarget", "conf_target"]: - assert_raises_rpc_error(-8, error_both, rbf_node.bumpfee, rbfid, {field: 1, "fee_rate": NORMAL}) - too_high = "is too high (cannot be higher than -maxtxfee" - assert_raises_rpc_error(-4, too_high, lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": BTC_MODE, "conf_target": 2009})) - assert_raises_rpc_error(-4, too_high, lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": SAT_MODE, "conf_target": 2009 * 10000})) - - self.log.info("Test invalid estimate_mode settings") - for k, v in {"number": 42, "object": {"foo": "bar"}}.items(): - assert_raises_rpc_error(-3, "Expected type string for estimate_mode, got {}".format(k), - lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": v, "fee_rate": NORMAL})) - for mode in ["foo", Decimal("3.141592")]: - assert_raises_rpc_error(-8, "Invalid estimate_mode parameter", - lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": mode, "fee_rate": NORMAL})) - - self.log.info("Test invalid fee_rate settings") - for mode in ["unset", "economical", "conservative", BTC_MODE, SAT_MODE]: - self.log.debug("{}".format(mode)) - for k, v in {"string": "", "object": {"foo": "bar"}}.items(): - assert_raises_rpc_error(-3, "Expected type number for fee_rate, got {}".format(k), - lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": mode, "fee_rate": v})) - assert_raises_rpc_error(-3, "Amount out of range", - lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": mode, "fee_rate": -1})) - assert_raises_rpc_error(-8, "Invalid fee_rate 0.00000000 BTC/kB (must be greater than 0)", - lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": mode, "fee_rate": 0})) - self.clear_mempool() - def test_segwit_bumpfee_succeeds(self, rbf_node, dest_address): self.log.info('Test that segwit-sourcing bumpfee works') # Create a transaction with segwit output, then create an RBF transaction From 3ac7b0c6f1c68e74a84d868a454f508bada6b09d Mon Sep 17 00:00:00 2001 From: Jon Atack Date: Mon, 19 Oct 2020 11:35:10 +0200 Subject: [PATCH 05/11] wallet: fundrawtx fee rate coverage, fixup ParseConfirmTarget() --- src/rpc/util.cpp | 9 ++-- test/functional/rpc_fundrawtransaction.py | 55 +++++++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/rpc/util.cpp b/src/rpc/util.cpp index 40dfdb587e..1b21587b6d 100644 --- a/src/rpc/util.cpp +++ b/src/rpc/util.cpp @@ -272,11 +272,12 @@ UniValue DescribeAddress(const CTxDestination& dest) unsigned int ParseConfirmTarget(const UniValue& value, unsigned int max_target) { - int target = value.get_int(); - if (target < 1 || (unsigned int)target > max_target) { - throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid conf_target, must be between %u - %u", 1, max_target)); + const int target{value.get_int()}; + const unsigned int unsigned_target{static_cast(target)}; + if (target < 1 || unsigned_target > max_target) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid conf_target, must be between %u and %u", 1, max_target)); } - return (unsigned int)target; + return unsigned_target; } RPCErrorCode RPCErrorFromTransactionError(TransactionError terr) diff --git a/test/functional/rpc_fundrawtransaction.py b/test/functional/rpc_fundrawtransaction.py index 7a729f7bc1..85ecb40354 100755 --- a/test/functional/rpc_fundrawtransaction.py +++ b/test/functional/rpc_fundrawtransaction.py @@ -7,6 +7,7 @@ from decimal import Decimal from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( + assert_approx, assert_equal, assert_fee_amount, assert_greater_than, @@ -89,6 +90,7 @@ def run_test(self): self.test_op_return() self.test_watchonly() self.test_all_watched_funds() + self.test_feerate_with_conf_target_and_estimate_mode() self.test_option_feerate() self.test_address_reuse() self.test_option_subtract_fee_from_outputs() @@ -672,6 +674,59 @@ def test_option_feerate(self): assert_fee_amount(result2['fee'], count_bytes(result2['hex']), 2 * result_fee_rate) assert_fee_amount(result3['fee'], count_bytes(result3['hex']), 10 * result_fee_rate) + def test_feerate_with_conf_target_and_estimate_mode(self): + self.log.info("Test fundrawtxn passing an explicit fee rate using conf_target and estimate_mode") + node = self.nodes[3] + # Make sure there is exactly one input so coin selection can't skew the result. + assert_equal(len(node.listunspent(1)), 1) + inputs = [] + outputs = {node.getnewaddress() : 1} + rawtx = node.createrawtransaction(inputs, outputs) + + for unit, fee_rate in {"btc/kb": 0.1, "sat/b": 10000}.items(): + self.log.info("Test fundrawtxn with conf_target {} estimate_mode {} produces expected fee".format(fee_rate, unit)) + # With no arguments passed, expect fee of 141 sats/b. + assert_approx(node.fundrawtransaction(rawtx)["fee"], vexp=0.00000141, vspan=0.00000001) + # Expect fee to be 10,000x higher when explicit fee 10,000x greater is specified. + result = node.fundrawtransaction(rawtx, {"conf_target": fee_rate, "estimate_mode": unit}) + assert_approx(result["fee"], vexp=0.0141, vspan=0.0001) + + for field, fee_rate in {"conf_target": 0.1, "estimate_mode": "sat/b"}.items(): + self.log.info("Test fundrawtxn raises RPC error if both feeRate and {} are passed".format(field)) + assert_raises_rpc_error( + -8, "Cannot specify both {} and feeRate".format(field), + lambda: node.fundrawtransaction(rawtx, {"feeRate": 0.1, field: fee_rate})) + + self.log.info("Test fundrawtxn with invalid estimate_mode settings") + for k, v in {"number": 42, "object": {"foo": "bar"}}.items(): + assert_raises_rpc_error(-3, "Expected type string for estimate_mode, got {}".format(k), + lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": v, "conf_target": 0.1})) + for mode in ["foo", Decimal("3.141592")]: + assert_raises_rpc_error(-8, "Invalid estimate_mode parameter", + lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": mode, "conf_target": 0.1})) + + self.log.info("Test fundrawtxn with invalid conf_target settings") + for mode in ["unset", "economical", "conservative", "btc/kb", "sat/b"]: + self.log.debug("{}".format(mode)) + for k, v in {"string": "", "object": {"foo": "bar"}}.items(): + assert_raises_rpc_error(-3, "Expected type number for conf_target, got {}".format(k), + lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": mode, "conf_target": v})) + if mode in ["btc/kb", "sat/b"]: + assert_raises_rpc_error(-3, "Amount out of range", + lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": mode, "conf_target": -1})) + assert_raises_rpc_error(-4, "Fee rate (0.00000000 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)", + lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": mode, "conf_target": 0})) + else: + for n in [-1, 0, 1009]: + assert_raises_rpc_error(-8, "Invalid conf_target, must be between 1 and 1008", + lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": mode, "conf_target": n})) + + for unit, fee_rate in {"sat/B": 0.99999999, "BTC/kB": 0.00000999}.items(): + self.log.info("- raises RPC error 'fee rate too low' if conf_target {} and estimate_mode {} are passed".format(fee_rate, unit)) + assert_raises_rpc_error(-4, "Fee rate (0.00000999 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)", + lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": unit, "conf_target": fee_rate, "add_inputs": True})) + + def test_address_reuse(self): """Test no address reuse occurs.""" self.log.info("Test fundrawtxn does not reuse addresses") From 6e1ea4273e52fdcd86c87628aa595c03a071ca8c Mon Sep 17 00:00:00 2001 From: Jon Atack Date: Fri, 26 Jun 2020 16:05:03 +0200 Subject: [PATCH 06/11] test: refactor for walletcreatefundedpsbt fee rate coverage --- test/functional/rpc_psbt.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index 10aebc2b1d..2bac77de81 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -174,8 +174,11 @@ def run_test(self): elif out['scriptPubKey']['addresses'][0] == p2pkh: p2pkh_pos = out['n'] + inputs = [{"txid": txid, "vout": p2wpkh_pos}, {"txid": txid, "vout": p2sh_p2wpkh_pos}, {"txid": txid, "vout": p2pkh_pos}] + outputs = [{self.nodes[1].getnewaddress(): 29.99}] + # spend single key from node 1 - created_psbt = self.nodes[1].walletcreatefundedpsbt([{"txid":txid,"vout":p2wpkh_pos},{"txid":txid,"vout":p2sh_p2wpkh_pos},{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():29.99}) + created_psbt = self.nodes[1].walletcreatefundedpsbt(inputs, outputs) walletprocesspsbt_out = self.nodes[1].walletprocesspsbt(created_psbt['psbt']) # Make sure it has both types of UTXOs decoded = self.nodes[1].decodepsbt(walletprocesspsbt_out['psbt']) @@ -186,15 +189,17 @@ def run_test(self): assert_equal(walletprocesspsbt_out['complete'], True) self.nodes[1].sendrawtransaction(self.nodes[1].finalizepsbt(walletprocesspsbt_out['psbt'])['hex']) - # feeRate of 0.1 BTC / KB produces a total fee slightly below -maxtxfee (~0.05280000): - res = self.nodes[1].walletcreatefundedpsbt([{"txid":txid,"vout":p2wpkh_pos},{"txid":txid,"vout":p2sh_p2wpkh_pos},{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():29.99}, 0, {"feeRate": 0.1, "add_inputs": True}) + self.log.info("Test walletcreatefundedpsbt feeRate of 0.1 BTC/kB produces a total fee at or slightly below -maxtxfee (~0.05290000)") + res = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"feeRate": 0.1, "add_inputs": True}) assert_approx(res["fee"], 0.055, 0.005) - # feeRate of 10 BTC / KB produces a total fee well above -maxtxfee + self.log.info("Test walletcreatefundedpsbt feeRate of 10 BTC/KB produces total fee well above -maxtxfee and raises RPC error") # previously this was silently capped at -maxtxfee - assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", self.nodes[1].walletcreatefundedpsbt, [{"txid":txid,"vout":p2wpkh_pos},{"txid":txid,"vout":p2sh_p2wpkh_pos},{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():29.99}, 0, {"feeRate": 10, "add_inputs": True}) - assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", self.nodes[1].walletcreatefundedpsbt, [{"txid":txid,"vout":p2wpkh_pos},{"txid":txid,"vout":p2sh_p2wpkh_pos},{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():1}, 0, {"feeRate": 10, "add_inputs": False}) + for bool_add, outputs_array in {True: outputs, False: [{self.nodes[1].getnewaddress(): 1}]}.items(): + assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", + self.nodes[1].walletcreatefundedpsbt, inputs, outputs_array, 0, {"feeRate": 10, "add_inputs": bool_add}) + self.log.info("Test various PSBT operations") # partially sign multisig things with node 1 psbtx = wmulti.walletcreatefundedpsbt(inputs=[{"txid":txid,"vout":p2wsh_pos},{"txid":txid,"vout":p2sh_pos},{"txid":txid,"vout":p2sh_p2wsh_pos}], outputs={self.nodes[1].getnewaddress():29.99}, options={'changeAddress': self.nodes[1].getrawchangeaddress()})['psbt'] walletprocesspsbt_out = self.nodes[1].walletprocesspsbt(psbtx) From 44e7bfa60313e4ae67da49e5ba4535038b71b453 Mon Sep 17 00:00:00 2001 From: Jon Atack Date: Fri, 26 Jun 2020 16:06:43 +0200 Subject: [PATCH 07/11] wallet: add walletcreatefundedpsbt explicit fee rate coverage --- test/functional/rpc_psbt.py | 47 ++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index 2bac77de81..28bcc516c6 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -193,7 +193,52 @@ def run_test(self): res = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"feeRate": 0.1, "add_inputs": True}) assert_approx(res["fee"], 0.055, 0.005) - self.log.info("Test walletcreatefundedpsbt feeRate of 10 BTC/KB produces total fee well above -maxtxfee and raises RPC error") + self.log.info("Test walletcreatefundedpsbt explicit fee rate with conf_target and estimate_mode") + for unit, fee_rate in {"btc/kb": 0.1, "sat/b": 10000}.items(): + fee = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"conf_target": fee_rate, "estimate_mode": unit, "add_inputs": True})["fee"] + self.log.info("- conf_target {}, estimate_mode {} produces fee {} at or slightly below -maxtxfee (~0.05290000)".format(fee_rate, unit, fee)) + assert_approx(fee, vexp=0.055, vspan=0.005) + + for field, fee_rate in {"conf_target": 0.1, "estimate_mode": "sat/b"}.items(): + self.log.info("- raises RPC error if both feeRate and {} are passed".format(field)) + assert_raises_rpc_error(-8, "Cannot specify both {} and feeRate".format(field), + lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"feeRate": 0.1, field: fee_rate, "add_inputs": True})) + + self.log.info("- raises RPC error with invalid estimate_mode settings") + for k, v in {"number": 42, "object": {"foo": "bar"}}.items(): + assert_raises_rpc_error(-3, "Expected type string for estimate_mode, got {}".format(k), + lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": v, "conf_target": 0.1, "add_inputs": True})) + for mode in ["foo", Decimal("3.141592")]: + assert_raises_rpc_error(-8, "Invalid estimate_mode parameter", + lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": mode, "conf_target": 0.1, "add_inputs": True})) + + self.log.info("- raises RPC error if estimate_mode is passed without a conf_target") + for unit in ["SAT/B", "BTC/KB"]: + assert_raises_rpc_error(-8, "Selected estimate_mode {} requires a fee rate to be specified in conf_target".format(unit), + lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": unit})) + + self.log.info("- raises RPC error with invalid conf_target settings") + for mode in ["unset", "economical", "conservative", "btc/kb", "sat/b"]: + self.log.debug("{}".format(mode)) + for k, v in {"string": "", "object": {"foo": "bar"}}.items(): + assert_raises_rpc_error(-3, "Expected type number for conf_target, got {}".format(k), + lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": mode, "conf_target": v, "add_inputs": True})) + if mode in ["btc/kb", "sat/b"]: + assert_raises_rpc_error(-3, "Amount out of range", + lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": mode, "conf_target": -1, "add_inputs": True})) + assert_raises_rpc_error(-4, "Fee rate (0.00000000 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)", + lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": mode, "conf_target": 0, "add_inputs": True})) + else: + for n in [-1, 0, 1009]: + assert_raises_rpc_error(-8, "Invalid conf_target, must be between 1 and 1008", + lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": mode, "conf_target": n, "add_inputs": True})) + + for unit, fee_rate in {"SAT/B": 0.99999999, "BTC/KB": 0.00000999}.items(): + self.log.info("- raises RPC error 'fee rate too low' if conf_target {} and estimate_mode {} are passed".format(fee_rate, unit)) + assert_raises_rpc_error(-4, "Fee rate (0.00000999 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)", + lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": unit, "conf_target": fee_rate, "add_inputs": True})) + + self.log.info("Test walletcreatefundedpsbt feeRate of 10 BTC/kB produces total fee well above -maxtxfee and raises RPC error") # previously this was silently capped at -maxtxfee for bool_add, outputs_array in {True: outputs, False: [{self.nodes[1].getnewaddress(): 1}]}.items(): assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", From dd341e602d5160fc621c0299179b91403756b61d Mon Sep 17 00:00:00 2001 From: Jon Atack Date: Sun, 25 Oct 2020 20:52:17 +0100 Subject: [PATCH 08/11] wallet: add sendtoaddress/sendmany explicit fee rate coverage --- test/functional/wallet_basic.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/test/functional/wallet_basic.py b/test/functional/wallet_basic.py index 7edcf41bea..411ae3db29 100755 --- a/test/functional/wallet_basic.py +++ b/test/functional/wallet_basic.py @@ -210,6 +210,8 @@ def run_test(self): assert_equal(self.nodes[2].getbalance(), node_2_bal) node_0_bal = self.check_fee_amount(self.nodes[0].getbalance(), Decimal('20'), fee_per_byte, self.get_vsize(self.nodes[2].gettransaction(txid)['hex'])) + self.log.info("Test sendmany") + # Sendmany 10 BTC txid = self.nodes[2].sendmany('', {address: 10}, 0, "", []) self.nodes[2].generate(1) @@ -226,7 +228,7 @@ def run_test(self): assert_equal(self.nodes[2].getbalance(), node_2_bal) node_0_bal = self.check_fee_amount(self.nodes[0].getbalance(), node_0_bal + Decimal('10'), fee_per_byte, self.get_vsize(self.nodes[2].gettransaction(txid)['hex'])) - # Sendmany with explicit fee (BTC/kB) + self.log.info("Test case-insensitive explicit fee rate (sendmany as BTC/kB)") # Throw if no conf_target provided assert_raises_rpc_error(-8, "Selected estimate_mode bTc/kB requires a fee rate to be specified in conf_target", self.nodes[2].sendmany, @@ -252,7 +254,7 @@ def run_test(self): node_0_bal += Decimal('10') assert_equal(self.nodes[0].getbalance(), node_0_bal) - # Sendmany with explicit fee (SAT/B) + self.log.info("Test case-insensitive explicit fee rate (sendmany as sat/B)") # Throw if no conf_target provided assert_raises_rpc_error(-8, "Selected estimate_mode sat/b requires a fee rate to be specified in conf_target", self.nodes[2].sendmany, @@ -280,6 +282,12 @@ def run_test(self): node_0_bal += Decimal('10') assert_equal(self.nodes[0].getbalance(), node_0_bal) + # Test setting explicit fee rate just below the minimum. + for unit, fee_rate in {"BTC/kB": 0.00000999, "sat/B": 0.99999999}.items(): + self.log.info("Test sendmany raises 'fee rate too low' if conf_target {} and estimate_mode {} are passed".format(fee_rate, unit)) + assert_raises_rpc_error(-6, "Fee rate (0.00000999 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)", + self.nodes[2].sendmany, amounts={address: 10}, estimate_mode=unit, conf_target=fee_rate) + self.start_node(3, self.nodes[3].extra_args) connect_nodes(self.nodes[0], 3) self.sync_all() @@ -413,8 +421,7 @@ def run_test(self): self.nodes[0].generate(1) self.sync_all(self.nodes[0:3]) - # send with explicit btc/kb fee - self.log.info("test explicit fee (sendtoaddress as btc/kb)") + self.log.info("Test case-insensitive explicit fee rate (sendtoaddress as BTC/kB)") self.nodes[0].generate(1) self.sync_all(self.nodes[0:3]) prebalance = self.nodes[2].getbalance() @@ -447,9 +454,9 @@ def run_test(self): fee = prebalance - postbalance - Decimal('1') assert_fee_amount(fee, tx_size, Decimal('0.00002500')) - # send with explicit sat/b fee self.sync_all(self.nodes[0:3]) - self.log.info("test explicit fee (sendtoaddress as sat/b)") + + self.log.info("Test case-insensitive explicit fee rate (sendtoaddress as sat/B)") self.nodes[0].generate(1) prebalance = self.nodes[2].getbalance() assert prebalance > 2 @@ -481,6 +488,12 @@ def run_test(self): fee = prebalance - postbalance - Decimal('1') assert_fee_amount(fee, tx_size, Decimal('0.00002000')) + # Test setting explicit fee rate just below the minimum. + for unit, fee_rate in {"BTC/kB": 0.00000999, "sat/B": 0.99999999}.items(): + self.log.info("Test sendtoaddress raises 'fee rate too low' if conf_target {} and estimate_mode {} are passed".format(fee_rate, unit)) + assert_raises_rpc_error(-6, "Fee rate (0.00000999 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)", + self.nodes[2].sendtoaddress, address=address, amount=1, estimate_mode=unit, conf_target=fee_rate) + # 2. Import address from node2 to node1 self.nodes[1].importaddress(address_to_import) From 603c0050837ec65765208dd54dde354145fbe098 Mon Sep 17 00:00:00 2001 From: Jon Atack Date: Sun, 25 Oct 2020 20:53:38 +0100 Subject: [PATCH 09/11] wallet: add rpc send explicit fee rate coverage --- test/functional/wallet_send.py | 70 ++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 12 deletions(-) diff --git a/test/functional/wallet_send.py b/test/functional/wallet_send.py index 876eb7f29e..5840a24404 100755 --- a/test/functional/wallet_send.py +++ b/test/functional/wallet_send.py @@ -97,6 +97,8 @@ def test_send(self, from_wallet, to_wallet=None, amount=None, data=None, except AssertionError: # Provide debug info if the test fails self.log.error("Unexpected successful result:") + self.log.error(arg_conf_target) + self.log.error(arg_estimate_mode) self.log.error(options) res = from_wallet.send(outputs=outputs, conf_target=arg_conf_target, estimate_mode=arg_estimate_mode, options=options) self.log.error(res) @@ -224,8 +226,10 @@ def run_test(self): assert_equal(self.nodes[1].decodepsbt(res1["psbt"])["fee"], self.nodes[1].decodepsbt(res2["psbt"])["fee"]) # but not at the same time - self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_conf_target=1, arg_estimate_mode="economical", - conf_target=1, estimate_mode="economical", add_to_wallet=False, expect_error=(-8,"Use either conf_target and estimate_mode or the options dictionary to control fee rate")) + for mode in ["unset", "economical", "conservative", "btc/kb", "sat/b"]: + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_conf_target=1, arg_estimate_mode="economical", + conf_target=1, estimate_mode=mode, add_to_wallet=False, + expect_error=(-8, "Use either conf_target and estimate_mode or the options dictionary to control fee rate")) self.log.info("Create PSBT from watch-only wallet w3, sign with w2...") res = self.test_send(from_wallet=w3, to_wallet=w1, amount=1) @@ -246,19 +250,61 @@ def run_test(self): res = w2.walletprocesspsbt(res["psbt"]) assert res["complete"] - self.log.info("Set fee rate...") + self.log.info("Test setting explicit fee rate") + res1 = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_conf_target=1, arg_estimate_mode="economical", add_to_wallet=False) + res2 = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=1, estimate_mode="economical", add_to_wallet=False) + assert_equal(self.nodes[1].decodepsbt(res1["psbt"])["fee"], self.nodes[1].decodepsbt(res2["psbt"])["fee"]) + + res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0.00007, estimate_mode="btc/kb", add_to_wallet=False) + fee = self.nodes[1].decodepsbt(res["psbt"])["fee"] + assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00007")) + res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=2, estimate_mode="sat/b", add_to_wallet=False) fee = self.nodes[1].decodepsbt(res["psbt"])["fee"] assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00002")) - self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=-1, estimate_mode="sat/b", - expect_error=(-3, "Amount out of range")) - # Fee rate of 0.1 satoshi per byte should throw an error - # TODO: error should use sat/b - self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0.1, estimate_mode="sat/b", - expect_error=(-4, "Fee rate (0.00000100 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)")) - - self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0.000001, estimate_mode="BTC/KB", - expect_error=(-4, "Fee rate (0.00000100 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)")) + + res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_conf_target=0.00004531, arg_estimate_mode="btc/kb", add_to_wallet=False) + fee = self.nodes[1].decodepsbt(res["psbt"])["fee"] + assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00004531")) + + res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_conf_target=3, arg_estimate_mode="sat/b", add_to_wallet=False) + fee = self.nodes[1].decodepsbt(res["psbt"])["fee"] + assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00003")) + + # TODO: This test should pass with all modes, e.g. with the next line uncommented, for consistency with the other explicit feerate RPCs. + # for mode in ["unset", "economical", "conservative", "btc/kb", "sat/b"]: + for mode in ["btc/kb", "sat/b"]: + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=-1, estimate_mode=mode, + expect_error=(-3, "Amount out of range")) + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0, estimate_mode=mode, + expect_error=(-4, "Fee rate (0.00000000 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)")) + + for mode in ["foo", Decimal("3.141592")]: + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0.1, estimate_mode=mode, + expect_error=(-8, "Invalid estimate_mode parameter")) + # TODO: these 2 equivalent sends with an invalid estimate_mode arg should both fail, but they do not...why? + # self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_conf_target=0.1, arg_estimate_mode=mode, + # expect_error=(-8, "Invalid estimate_mode parameter")) + # assert_raises_rpc_error(-8, "Invalid estimate_mode parameter", lambda: w0.send({w1.getnewaddress(): 1}, 0.1, mode)) + + # TODO: These tests should pass for consistency with the other explicit feerate RPCs, but they do not. + # for mode in ["unset", "economical", "conservative", "btc/kb", "sat/b"]: + # self.log.debug("{}".format(mode)) + # for k, v in {"string": "", "object": {"foo": "bar"}}.items(): + # self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=v, estimate_mode=mode, + # expect_error=(-3, "Expected type number for conf_target, got {}".format(k))) + + # TODO: error should use sat/B instead of BTC/kB if sat/B is selected. + # Test setting explicit fee rate just below the minimum. + for unit, fee_rate in {"sat/B": 0.99999999, "BTC/kB": 0.00000999}.items(): + self.log.info("Explicit fee rate raises RPC error 'fee rate too low' if conf_target {} and estimate_mode {} are passed".format(fee_rate, unit)) + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=fee_rate, estimate_mode=unit, + expect_error=(-4, "Fee rate (0.00000999 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)")) + + self.log.info("Explicit fee rate raises RPC error if estimate_mode is passed without a conf_target") + for unit, fee_rate in {"sat/B": 100, "BTC/kB": 0.001}.items(): + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, estimate_mode=unit, + expect_error=(-8, "Selected estimate_mode {} requires a fee rate to be specified in conf_target".format(unit))) # TODO: Return hex if fee rate is below -maxmempool # res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0.1, estimate_mode="sat/b", add_to_wallet=False) From 778b9be40667876c705e586849ea9c9e44cf451c Mon Sep 17 00:00:00 2001 From: Jon Atack Date: Mon, 19 Oct 2020 14:23:35 +0200 Subject: [PATCH 10/11] wallet, rpc: fix send subtract_fee_from_outputs help --- src/wallet/rpcwallet.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 9bb09dda16..02d7fce94e 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -4034,7 +4034,7 @@ static RPCHelpMan send() {"locktime", RPCArg::Type::NUM, /* default */ "0", "Raw locktime. Non-0 value also locktime-activates inputs"}, {"lock_unspents", RPCArg::Type::BOOL, /* default */ "false", "Lock selected unspent outputs"}, {"psbt", RPCArg::Type::BOOL, /* default */ "automatic", "Always return a PSBT, implies add_to_wallet=false."}, - {"subtract_fee_from_outputs", RPCArg::Type::ARR, /* default */ "empty array", "A JSON array of integers.\n" + {"subtract_fee_from_outputs", RPCArg::Type::ARR, /* default */ "empty array", "Outputs to subtract the fee from, specified as integer indices.\n" "The fee will be equally deducted from the amount of each specified output.\n" "Those recipients will receive less bitcoins than you enter in their corresponding amount field.\n" "If no outputs are specified here, the sender pays the fee.", From 0be29000c011dec0722481dbebb159873da6fa54 Mon Sep 17 00:00:00 2001 From: Jon Atack Date: Thu, 22 Oct 2020 17:19:38 +0200 Subject: [PATCH 11/11] rpc: update conf_target helps for correctness/consistency for sendtoaddress, sendmany, send, fundrawtransaction, walletcreatefundedpsbt, bumpfee --- src/wallet/rpcwallet.cpp | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 02d7fce94e..d4255d9eac 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -440,7 +440,8 @@ static RPCHelpMan sendtoaddress() {"subtractfeefromamount", RPCArg::Type::BOOL, /* default */ "false", "The fee will be deducted from the amount being sent.\n" "The recipient will receive less bitcoins than you enter in the amount field."}, {"replaceable", RPCArg::Type::BOOL, /* default */ "wallet default", "Allow this transaction to be replaced by a transaction with higher fees via BIP 125"}, - {"conf_target", RPCArg::Type::NUM, /* default */ "wallet default", "Confirmation target (in blocks), or fee rate (for " + CURRENCY_UNIT + "/kB or " + CURRENCY_ATOM + "/B estimate modes)"}, + {"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target (in blocks)\n" + "or fee rate (for " + CURRENCY_UNIT + "/kB and " + CURRENCY_ATOM + "/B estimate modes)"}, {"estimate_mode", RPCArg::Type::STR, /* default */ "unset", std::string() + "The fee estimate mode, must be one of (case insensitive):\n" " \"" + FeeModes("\"\n\"") + "\""}, {"avoid_reuse", RPCArg::Type::BOOL, /* default */ "true", "(only available if avoid_reuse wallet flag is set) Avoid spending from dirty addresses; addresses are considered\n" @@ -868,7 +869,8 @@ static RPCHelpMan sendmany() }, }, {"replaceable", RPCArg::Type::BOOL, /* default */ "wallet default", "Allow this transaction to be replaced by a transaction with higher fees via BIP 125"}, - {"conf_target", RPCArg::Type::NUM, /* default */ "wallet default", "Confirmation target (in blocks), or fee rate (for " + CURRENCY_UNIT + "/kB or " + CURRENCY_ATOM + "/B estimate modes)"}, + {"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target (in blocks)\n" + "or fee rate (for " + CURRENCY_UNIT + "/kB and " + CURRENCY_ATOM + "/B estimate modes)"}, {"estimate_mode", RPCArg::Type::STR, /* default */ "unset", std::string() + "The fee estimate mode, must be one of (case insensitive):\n" " \"" + FeeModes("\"\n\"") + "\""}, {"verbose", RPCArg::Type::BOOL, /* default */ "false", "If true, return extra infomration about the transaction."}, @@ -3205,7 +3207,8 @@ static RPCHelpMan fundrawtransaction() }, {"replaceable", RPCArg::Type::BOOL, /* default */ "wallet default", "Marks this transaction as BIP125 replaceable.\n" "Allows this transaction to be replaced by a transaction with higher fees"}, - {"conf_target", RPCArg::Type::NUM, /* default */ "wallet default", "Confirmation target (in blocks), or fee rate (for " + CURRENCY_UNIT + "/kB or " + CURRENCY_ATOM + "/B estimate modes)"}, + {"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target (in blocks)\n" + "or fee rate (for " + CURRENCY_UNIT + "/kB and " + CURRENCY_ATOM + "/B estimate modes)"}, {"estimate_mode", RPCArg::Type::STR, /* default */ "unset", std::string() + "The fee estimate mode, must be one of (case insensitive):\n" " \"" + FeeModes("\"\n\"") + "\""}, }, @@ -3382,10 +3385,11 @@ static RPCHelpMan bumpfee_helper(std::string method_name) {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The txid to be bumped"}, {"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "", { - {"conf_target", RPCArg::Type::NUM, /* default */ "wallet default", "Confirmation target (in blocks)"}, - {"fee_rate", RPCArg::Type::NUM, /* default */ "fall back to 'conf_target'", "fee rate (NOT total fee) to pay, in " + CURRENCY_UNIT + " per kB\n" + {"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target (in blocks)\n" + "or fee rate (for " + CURRENCY_UNIT + "/kB and " + CURRENCY_ATOM + "/B estimate modes)"}, + {"fee_rate", RPCArg::Type::NUM, /* default */ "fall back to 'conf_target'", "fee rate (NOT total fee) to pay, in " + CURRENCY_UNIT + "/kB.\n" "Specify a fee rate instead of relying on the built-in fee estimator.\n" - "Must be at least 0.0001 " + CURRENCY_UNIT + " per kB higher than the current transaction fee rate.\n"}, + "Must be at least 0.0001 " + CURRENCY_UNIT + "/kB higher than the current transaction fee rate.\n"}, {"replaceable", RPCArg::Type::BOOL, /* default */ "true", "Whether the new transaction should still be\n" "marked bip-125 replaceable. If true, the sequence numbers in the transaction will\n" "be left unchanged from the original. If false, any input sequence numbers in the\n" @@ -4008,7 +4012,8 @@ static RPCHelpMan send() }, }, }, - {"conf_target", RPCArg::Type::NUM, /* default */ "wallet default", "Confirmation target (in blocks), or fee rate (for " + CURRENCY_UNIT + "/kB or " + CURRENCY_ATOM + "/B estimate modes)"}, + {"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target (in blocks)\n" + "or fee rate (for " + CURRENCY_UNIT + "/kB and " + CURRENCY_ATOM + "/B estimate modes)"}, {"estimate_mode", RPCArg::Type::STR, /* default */ "unset", std::string() + "The fee estimate mode, must be one of (case insensitive):\n" " \"" + FeeModes("\"\n\"") + "\""}, {"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "", @@ -4018,7 +4023,8 @@ static RPCHelpMan send() {"change_address", RPCArg::Type::STR_HEX, /* default */ "pool address", "The bitcoin address to receive the change"}, {"change_position", RPCArg::Type::NUM, /* default */ "random", "The index of the change output"}, {"change_type", RPCArg::Type::STR, /* default */ "set by -changetype", "The output type to use. Only valid if change_address is not specified. Options are \"legacy\", \"p2sh-segwit\", and \"bech32\"."}, - {"conf_target", RPCArg::Type::NUM, /* default */ "wallet default", "Confirmation target (in blocks), or fee rate (for " + CURRENCY_UNIT + "/kB or " + CURRENCY_ATOM + "/B estimate modes)"}, + {"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target (in blocks)\n" + "or fee rate (for " + CURRENCY_UNIT + "/kB and " + CURRENCY_ATOM + "/B estimate modes)"}, {"estimate_mode", RPCArg::Type::STR, /* default */ "unset", std::string() + "The fee estimate mode, must be one of (case insensitive):\n" " \"" + FeeModes("\"\n\"") + "\""}, {"include_watching", RPCArg::Type::BOOL, /* default */ "true for watch-only wallets, otherwise false", "Also select inputs which are watch only.\n" @@ -4356,7 +4362,8 @@ static RPCHelpMan walletcreatefundedpsbt() }, {"replaceable", RPCArg::Type::BOOL, /* default */ "wallet default", "Marks this transaction as BIP125 replaceable.\n" "Allows this transaction to be replaced by a transaction with higher fees"}, - {"conf_target", RPCArg::Type::NUM, /* default */ "fall back to wallet's confirmation target (txconfirmtarget)", "Confirmation target (in blocks)"}, + {"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target (in blocks)\n" + "or fee rate (for " + CURRENCY_UNIT + "/kB and " + CURRENCY_ATOM + "/B estimate modes)"}, {"estimate_mode", RPCArg::Type::STR, /* default */ "unset", std::string() + "The fee estimate mode, must be one of (case insensitive):\n" " \"" + FeeModes("\"\n\"") + "\""}, },