aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorAndrew Chow <github@achow101.com>2023-01-16 17:12:30 -0500
committerAndrew Chow <github@achow101.com>2023-01-16 17:23:51 -0500
commitb55b11f92a4717bfbe9214d134b1941effcb391a (patch)
treee50dfd610637ff409e36380b675bca348cfa0dda /test
parent599e941c194749dab35d81a4e898fd79dd2ed129 (diff)
parentcfe5aebc79c510bd2156e199c3324d7ee1f8d2ad (diff)
downloadbitcoin-b55b11f92a4717bfbe9214d134b1941effcb391a.tar.xz
Merge bitcoin/bitcoin#25375: rpc: add minconf/maxconf options to sendall and fund transaction calls
cfe5aebc79c510bd2156e199c3324d7ee1f8d2ad rpc: add minconf and maxconf options to sendall (ishaanam) a07a413466a0edd47eab9189b46a70aafbbe22b7 Wallet/RPC: Allow specifying min & max chain depth for inputs used by fund calls (Juan Pablo Civile) Pull request description: This PR adds a "minconf" option to `fundrawtransaction`, `walletcreatefundedpsbt`, and `sendall`. Alternative implementation of #14641 Fixes #14542 Edit: This PR now also adds this option to `send` ACKs for top commit: achow101: ACK cfe5aebc79c510bd2156e199c3324d7ee1f8d2ad Xekyo: ACK cfe5aebc79c510bd2156e199c3324d7ee1f8d2ad furszy: diff ACK cfe5aebc, only a non-blocking nit. Tree-SHA512: 836e610926eec3a62308fba88ddbd6a13d8f4dac37352d0309599f893cde9c1df5e9c298fda6e076493068e4d213e4afa7290a9e3bdb5a95a5d507da3f7b59e8
Diffstat (limited to 'test')
-rwxr-xr-xtest/functional/rpc_psbt.py62
-rwxr-xr-xtest/functional/wallet_fundrawtransaction.py61
-rwxr-xr-xtest/functional/wallet_send.py14
-rwxr-xr-xtest/functional/wallet_sendall.py68
4 files changed, 204 insertions, 1 deletions
diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py
index a50e0fb244..58a80e37a2 100755
--- a/test/functional/rpc_psbt.py
+++ b/test/functional/rpc_psbt.py
@@ -36,6 +36,7 @@ from test_framework.util import (
assert_approx,
assert_equal,
assert_greater_than,
+ assert_greater_than_or_equal,
assert_raises_rpc_error,
find_output,
find_vout_for_address,
@@ -106,6 +107,65 @@ class PSBTTest(BitcoinTestFramework):
self.connect_nodes(0, 1)
self.connect_nodes(0, 2)
+ def test_input_confs_control(self):
+ self.nodes[0].createwallet("minconf")
+ wallet = self.nodes[0].get_wallet_rpc("minconf")
+
+ # Fund the wallet with different chain heights
+ for _ in range(2):
+ self.nodes[1].sendmany("", {wallet.getnewaddress():1, wallet.getnewaddress():1})
+ self.generate(self.nodes[1], 1)
+
+ unconfirmed_txid = wallet.sendtoaddress(wallet.getnewaddress(), 0.5)
+
+ self.log.info("Crafting PSBT using an unconfirmed input")
+ target_address = self.nodes[1].getnewaddress()
+ psbtx1 = wallet.walletcreatefundedpsbt([], {target_address: 0.1}, 0, {'fee_rate': 1, 'maxconf': 0})['psbt']
+
+ # Make sure we only had the one input
+ tx1_inputs = self.nodes[0].decodepsbt(psbtx1)['tx']['vin']
+ assert_equal(len(tx1_inputs), 1)
+
+ utxo1 = tx1_inputs[0]
+ assert_equal(unconfirmed_txid, utxo1['txid'])
+
+ signed_tx1 = wallet.walletprocesspsbt(psbtx1)['psbt']
+ final_tx1 = wallet.finalizepsbt(signed_tx1)['hex']
+ txid1 = self.nodes[0].sendrawtransaction(final_tx1)
+
+ mempool = self.nodes[0].getrawmempool()
+ assert txid1 in mempool
+
+ self.log.info("Fail to craft a new PSBT that sends more funds with add_inputs = False")
+ assert_raises_rpc_error(-4, "The preselected coins total amount does not cover the transaction target. Please allow other inputs to be automatically selected or include more coins manually", wallet.walletcreatefundedpsbt, [{'txid': utxo1['txid'], 'vout': utxo1['vout']}], {target_address: 1}, 0, {'add_inputs': False})
+
+ self.log.info("Fail to craft a new PSBT with minconf above highest one")
+ assert_raises_rpc_error(-4, "Insufficient funds", wallet.walletcreatefundedpsbt, [{'txid': utxo1['txid'], 'vout': utxo1['vout']}], {target_address: 1}, 0, {'add_inputs': True, 'minconf': 3, 'fee_rate': 10})
+
+ self.log.info("Fail to broadcast a new PSBT with maxconf 0 due to BIP125 rules to verify it actually chose unconfirmed outputs")
+ psbt_invalid = wallet.walletcreatefundedpsbt([{'txid': utxo1['txid'], 'vout': utxo1['vout']}], {target_address: 1}, 0, {'add_inputs': True, 'maxconf': 0, 'fee_rate': 10})['psbt']
+ signed_invalid = wallet.walletprocesspsbt(psbt_invalid)['psbt']
+ final_invalid = wallet.finalizepsbt(signed_invalid)['hex']
+ assert_raises_rpc_error(-26, "bad-txns-spends-conflicting-tx", self.nodes[0].sendrawtransaction, final_invalid)
+
+ self.log.info("Craft a replacement adding inputs with highest confs possible")
+ psbtx2 = wallet.walletcreatefundedpsbt([{'txid': utxo1['txid'], 'vout': utxo1['vout']}], {target_address: 1}, 0, {'add_inputs': True, 'minconf': 2, 'fee_rate': 10})['psbt']
+ tx2_inputs = self.nodes[0].decodepsbt(psbtx2)['tx']['vin']
+ assert_greater_than_or_equal(len(tx2_inputs), 2)
+ for vin in tx2_inputs:
+ if vin['txid'] != unconfirmed_txid:
+ assert_greater_than_or_equal(self.nodes[0].gettxout(vin['txid'], vin['vout'])['confirmations'], 2)
+
+ signed_tx2 = wallet.walletprocesspsbt(psbtx2)['psbt']
+ final_tx2 = wallet.finalizepsbt(signed_tx2)['hex']
+ txid2 = self.nodes[0].sendrawtransaction(final_tx2)
+
+ mempool = self.nodes[0].getrawmempool()
+ assert txid1 not in mempool
+ assert txid2 in mempool
+
+ wallet.unloadwallet()
+
def assert_change_type(self, psbtx, expected_type):
"""Assert that the given PSBT has a change output with the given type."""
@@ -514,6 +574,8 @@ class PSBTTest(BitcoinTestFramework):
# TODO: Re-enable this for segwit v1
# self.test_utxo_conversion()
+ self.test_input_confs_control()
+
# Test that psbts with p2pkh outputs are created properly
p2pkh = self.nodes[0].getnewaddress(address_type='legacy')
psbt = self.nodes[1].walletcreatefundedpsbt([], [{p2pkh : 1}], 0, {"includeWatching" : True}, True)
diff --git a/test/functional/wallet_fundrawtransaction.py b/test/functional/wallet_fundrawtransaction.py
index 98b0f70b01..29ddb77b41 100755
--- a/test/functional/wallet_fundrawtransaction.py
+++ b/test/functional/wallet_fundrawtransaction.py
@@ -148,6 +148,7 @@ class RawTransactionsTest(BitcoinTestFramework):
self.test_external_inputs()
self.test_22670()
self.test_feerate_rounding()
+ self.test_input_confs_control()
def test_change_position(self):
"""Ensure setting changePosition in fundraw with an exact match is handled properly."""
@@ -1403,6 +1404,66 @@ class RawTransactionsTest(BitcoinTestFramework):
rawtx = w.createrawtransaction(inputs=[], outputs=[{self.nodes[0].getnewaddress(address_type="bech32"): 1 - 0.00000202}])
assert_raises_rpc_error(-4, "Insufficient funds", w.fundrawtransaction, rawtx, {"fee_rate": 1.85})
+ def test_input_confs_control(self):
+ self.nodes[0].createwallet("minconf")
+ wallet = self.nodes[0].get_wallet_rpc("minconf")
+
+ # Fund the wallet with different chain heights
+ for _ in range(2):
+ self.nodes[2].sendmany("", {wallet.getnewaddress():1, wallet.getnewaddress():1})
+ self.generate(self.nodes[2], 1)
+
+ unconfirmed_txid = wallet.sendtoaddress(wallet.getnewaddress(), 0.5)
+
+ self.log.info("Crafting TX using an unconfirmed input")
+ target_address = self.nodes[2].getnewaddress()
+ raw_tx1 = wallet.createrawtransaction([], {target_address: 0.1}, 0, True)
+ funded_tx1 = wallet.fundrawtransaction(raw_tx1, {'fee_rate': 1, 'maxconf': 0})['hex']
+
+ # Make sure we only had the one input
+ tx1_inputs = self.nodes[0].decoderawtransaction(funded_tx1)['vin']
+ assert_equal(len(tx1_inputs), 1)
+
+ utxo1 = tx1_inputs[0]
+ assert unconfirmed_txid == utxo1['txid']
+
+ final_tx1 = wallet.signrawtransactionwithwallet(funded_tx1)['hex']
+ txid1 = self.nodes[0].sendrawtransaction(final_tx1)
+
+ mempool = self.nodes[0].getrawmempool()
+ assert txid1 in mempool
+
+ self.log.info("Fail to craft a new TX with minconf above highest one")
+ # Create a replacement tx to 'final_tx1' that has 1 BTC target instead of 0.1.
+ raw_tx2 = wallet.createrawtransaction([{'txid': utxo1['txid'], 'vout': utxo1['vout']}], {target_address: 1})
+ assert_raises_rpc_error(-4, "Insufficient funds", wallet.fundrawtransaction, raw_tx2, {'add_inputs': True, 'minconf': 3, 'fee_rate': 10})
+
+ self.log.info("Fail to broadcast a new TX with maxconf 0 due to BIP125 rules to verify it actually chose unconfirmed outputs")
+ # Now fund 'raw_tx2' to fulfill the total target (1 BTC) by using all the wallet unconfirmed outputs.
+ # As it was created with the first unconfirmed output, 'raw_tx2' only has 0.1 BTC covered (need to fund 0.9 BTC more).
+ # So, the selection process, to cover the amount, will pick up the 'final_tx1' output as well, which is an output of the tx that this
+ # new tx is replacing!. So, once we send it to the mempool, it will return a "bad-txns-spends-conflicting-tx"
+ # because the input will no longer exist once the first tx gets replaced by this new one).
+ funded_invalid = wallet.fundrawtransaction(raw_tx2, {'add_inputs': True, 'maxconf': 0, 'fee_rate': 10})['hex']
+ final_invalid = wallet.signrawtransactionwithwallet(funded_invalid)['hex']
+ assert_raises_rpc_error(-26, "bad-txns-spends-conflicting-tx", self.nodes[0].sendrawtransaction, final_invalid)
+
+ self.log.info("Craft a replacement adding inputs with highest depth possible")
+ funded_tx2 = wallet.fundrawtransaction(raw_tx2, {'add_inputs': True, 'minconf': 2, 'fee_rate': 10})['hex']
+ tx2_inputs = self.nodes[0].decoderawtransaction(funded_tx2)['vin']
+ assert_greater_than_or_equal(len(tx2_inputs), 2)
+ for vin in tx2_inputs:
+ if vin['txid'] != unconfirmed_txid:
+ assert_greater_than_or_equal(self.nodes[0].gettxout(vin['txid'], vin['vout'])['confirmations'], 2)
+
+ final_tx2 = wallet.signrawtransactionwithwallet(funded_tx2)['hex']
+ txid2 = self.nodes[0].sendrawtransaction(final_tx2)
+
+ mempool = self.nodes[0].getrawmempool()
+ assert txid1 not in mempool
+ assert txid2 in mempool
+
+ wallet.unloadwallet()
if __name__ == '__main__':
RawTransactionsTest().main()
diff --git a/test/functional/wallet_send.py b/test/functional/wallet_send.py
index 424834323f..ac3ec06eec 100755
--- a/test/functional/wallet_send.py
+++ b/test/functional/wallet_send.py
@@ -45,7 +45,7 @@ class WalletSendTest(BitcoinTestFramework):
conf_target=None, estimate_mode=None, fee_rate=None, add_to_wallet=None, psbt=None,
inputs=None, add_inputs=None, include_unsafe=None, change_address=None, change_position=None, change_type=None,
include_watching=None, locktime=None, lock_unspents=None, replaceable=None, subtract_fee_from_outputs=None,
- expect_error=None, solving_data=None):
+ expect_error=None, solving_data=None, minconf=None):
assert (amount is None) != (data is None)
from_balance_before = from_wallet.getbalances()["mine"]["trusted"]
@@ -106,6 +106,8 @@ class WalletSendTest(BitcoinTestFramework):
options["subtract_fee_from_outputs"] = subtract_fee_from_outputs
if solving_data is not None:
options["solving_data"] = solving_data
+ if minconf is not None:
+ options["minconf"] = minconf
if len(options.keys()) == 0:
options = None
@@ -487,6 +489,16 @@ class WalletSendTest(BitcoinTestFramework):
res = self.test_send(from_wallet=w5, to_wallet=w0, amount=1, include_unsafe=True)
assert res["complete"]
+ self.log.info("Minconf")
+ self.nodes[1].createwallet(wallet_name="minconfw")
+ minconfw= self.nodes[1].get_wallet_rpc("minconfw")
+ self.test_send(from_wallet=w0, to_wallet=minconfw, amount=2)
+ self.generate(self.nodes[0], 3)
+ self.test_send(from_wallet=minconfw, to_wallet=w0, amount=1, minconf=4, expect_error=(-4, "Insufficient funds"))
+ self.test_send(from_wallet=minconfw, to_wallet=w0, amount=1, minconf=-4, expect_error=(-8, "Negative minconf"))
+ res = self.test_send(from_wallet=minconfw, to_wallet=w0, amount=1, minconf=3)
+ assert res["complete"]
+
self.log.info("External outputs")
eckey = ECKey()
eckey.generate()
diff --git a/test/functional/wallet_sendall.py b/test/functional/wallet_sendall.py
index 778c8a5b9e..f6440f07d7 100755
--- a/test/functional/wallet_sendall.py
+++ b/test/functional/wallet_sendall.py
@@ -317,6 +317,68 @@ class SendallTest(BitcoinTestFramework):
assert_equal(decoded["tx"]["vin"][0]["vout"], utxo["vout"])
assert_equal(decoded["tx"]["vout"][0]["scriptPubKey"]["address"], self.remainder_target)
+ @cleanup
+ def sendall_with_minconf(self):
+ # utxo of 17 bicoin has 6 confirmations, utxo of 4 has 3
+ self.add_utxos([17])
+ self.generate(self.nodes[0], 2)
+ self.add_utxos([4])
+ self.generate(self.nodes[0], 2)
+
+ self.log.info("Test sendall fails because minconf is negative")
+
+ assert_raises_rpc_error(-8,
+ "Invalid minconf (minconf cannot be negative): -2",
+ self.wallet.sendall,
+ recipients=[self.remainder_target],
+ options={"minconf": -2})
+ self.log.info("Test sendall fails because minconf is used while specific inputs are provided")
+
+ utxo = self.wallet.listunspent()[0]
+ assert_raises_rpc_error(-8,
+ "Cannot combine minconf or maxconf with specific inputs.",
+ self.wallet.sendall,
+ recipients=[self.remainder_target],
+ options={"inputs": [utxo], "minconf": 2})
+
+ self.log.info("Test sendall fails because there are no utxos with enough confirmations specified by minconf")
+
+ assert_raises_rpc_error(-6,
+ "Total value of UTXO pool too low to pay for transaction. Try using lower feerate or excluding uneconomic UTXOs with 'send_max' option.",
+ self.wallet.sendall,
+ recipients=[self.remainder_target],
+ options={"minconf": 7})
+
+ self.log.info("Test sendall only spends utxos with a specified number of confirmations when minconf is used")
+ self.wallet.sendall(recipients=[self.remainder_target], fee_rate=300, options={"minconf": 6})
+
+ assert_equal(len(self.wallet.listunspent()), 1)
+ assert_equal(self.wallet.listunspent()[0]['confirmations'], 3)
+
+ # decrease minconf and show the remaining utxo is picked up
+ self.wallet.sendall(recipients=[self.remainder_target], fee_rate=300, options={"minconf": 3})
+ assert_equal(self.wallet.getbalance(), 0)
+
+ @cleanup
+ def sendall_with_maxconf(self):
+ # utxo of 17 bicoin has 6 confirmations, utxo of 4 has 3
+ self.add_utxos([17])
+ self.generate(self.nodes[0], 2)
+ self.add_utxos([4])
+ self.generate(self.nodes[0], 2)
+
+ self.log.info("Test sendall fails because there are no utxos with enough confirmations specified by maxconf")
+ assert_raises_rpc_error(-6,
+ "Total value of UTXO pool too low to pay for transaction. Try using lower feerate or excluding uneconomic UTXOs with 'send_max' option.",
+ self.wallet.sendall,
+ recipients=[self.remainder_target],
+ options={"maxconf": 1})
+
+ self.log.info("Test sendall only spends utxos with a specified number of confirmations when maxconf is used")
+ self.wallet.sendall(recipients=[self.remainder_target], fee_rate=300, options={"maxconf":4})
+ assert_equal(len(self.wallet.listunspent()), 1)
+ assert_equal(self.wallet.listunspent()[0]['confirmations'], 6)
+
# This tests needs to be the last one otherwise @cleanup will fail with "Transaction too large" error
def sendall_fails_with_transaction_too_large(self):
self.log.info("Test that sendall fails if resulting transaction is too large")
@@ -392,6 +454,12 @@ class SendallTest(BitcoinTestFramework):
# Sendall succeeds with watchonly wallets spending specific UTXOs
self.sendall_watchonly_specific_inputs()
+ # Sendall only uses outputs with at least a give number of confirmations when using minconf
+ self.sendall_with_minconf()
+
+ # Sendall only uses outputs with less than a given number of confirmation when using minconf
+ self.sendall_with_maxconf()
+
# Sendall fails when many inputs result to too large transaction
self.sendall_fails_with_transaction_too_large()