Skip to content

Commit 92bcd70

Browse files
committed
[wallet] allow transaction without change if keypool is empty
1 parent 709f868 commit 92bcd70

File tree

3 files changed

+70
-11
lines changed

3 files changed

+70
-11
lines changed

src/wallet/wallet.cpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2629,13 +2629,14 @@ bool CWallet::CreateTransaction(interfaces::Chain::Lock& locked_chain, const std
26292629
// rediscover unknown transactions that were written with keys of ours to recover
26302630
// post-backup change.
26312631

2632-
// Reserve a new key pair from key pool
2632+
// Reserve a new key pair from key pool. If it fails, provide a dummy
2633+
// destination in case we don't need change.
26332634
CTxDestination dest;
26342635
if (!reservedest.GetReservedDestination(dest, true)) {
2635-
strFailReason = _("Can't generate a change-address key. Please call keypoolrefill first.").translated;
2636-
return false;
2636+
strFailReason = _("Transaction needs a change address, but we can't generate it. Please call keypoolrefill first.").translated;
26372637
}
26382638
scriptChange = GetScriptForDestination(dest);
2639+
assert(!dest.empty() || scriptChange.empty());
26392640
}
26402641
CTxOut change_prototype_txout(0, scriptChange);
26412642
coin_selection_params.change_output_size = GetSerializeSize(change_prototype_txout);
@@ -2851,6 +2852,11 @@ bool CWallet::CreateTransaction(interfaces::Chain::Lock& locked_chain, const std
28512852
coin_selection_params.use_bnb = false;
28522853
continue;
28532854
}
2855+
2856+
// Give up if change keypool ran out and we failed to find a solution without change:
2857+
if (scriptChange.empty() && nChangePosInOut != -1) {
2858+
return false;
2859+
}
28542860
}
28552861

28562862
// Shuffle selected coins and fill in final vin

test/functional/rpc_fundrawtransaction.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -500,11 +500,16 @@ def test_locked_wallet(self):
500500
self.nodes[1].getnewaddress()
501501
self.nodes[1].getrawchangeaddress()
502502
inputs = []
503-
outputs = {self.nodes[0].getnewaddress():1.1}
503+
outputs = {self.nodes[0].getnewaddress():1.09999500}
504504
rawtx = self.nodes[1].createrawtransaction(inputs, outputs)
505+
# fund a transaction that does not require a new key for the change output
506+
self.nodes[1].fundrawtransaction(rawtx)
507+
505508
# fund a transaction that requires a new key for the change output
506509
# creating the key must be impossible because the wallet is locked
507-
assert_raises_rpc_error(-4, "Can't generate a change-address key. Please call keypoolrefill first.", self.nodes[1].fundrawtransaction, rawtx)
510+
outputs = {self.nodes[0].getnewaddress():1.1}
511+
rawtx = self.nodes[1].createrawtransaction(inputs, outputs)
512+
assert_raises_rpc_error(-4, "Transaction needs a change address, but we can't generate it. Please call keypoolrefill first.", self.nodes[1].fundrawtransaction, rawtx)
508513

509514
# Refill the keypool.
510515
self.nodes[1].walletpassphrase("test", 100)

test/functional/wallet_keypool.py

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""Test the wallet keypool and interaction with wallet encryption/locking."""
66

77
import time
8+
from decimal import Decimal
89

910
from test_framework.test_framework import BitcoinTestFramework
1011
from test_framework.util import assert_equal, assert_raises_rpc_error
@@ -53,12 +54,12 @@ def run_test(self):
5354
assert_raises_rpc_error(-12, "Keypool ran out", nodes[0].getrawchangeaddress)
5455

5556
# drain the external keys
56-
addr.add(nodes[0].getnewaddress())
57-
addr.add(nodes[0].getnewaddress())
58-
addr.add(nodes[0].getnewaddress())
59-
addr.add(nodes[0].getnewaddress())
60-
addr.add(nodes[0].getnewaddress())
61-
addr.add(nodes[0].getnewaddress())
57+
addr.add(nodes[0].getnewaddress(address_type="bech32"))
58+
addr.add(nodes[0].getnewaddress(address_type="bech32"))
59+
addr.add(nodes[0].getnewaddress(address_type="bech32"))
60+
addr.add(nodes[0].getnewaddress(address_type="bech32"))
61+
addr.add(nodes[0].getnewaddress(address_type="bech32"))
62+
addr.add(nodes[0].getnewaddress(address_type="bech32"))
6263
assert len(addr) == 6
6364
# the next one should fail
6465
assert_raises_rpc_error(-12, "Error: Keypool ran out, please call keypoolrefill first", nodes[0].getnewaddress)
@@ -82,5 +83,52 @@ def run_test(self):
8283
assert_equal(wi['keypoolsize_hd_internal'], 100)
8384
assert_equal(wi['keypoolsize'], 100)
8485

86+
# create a blank wallet
87+
nodes[0].createwallet(wallet_name='w2', blank=True)
88+
w2 = nodes[0].get_wallet_rpc('w2')
89+
90+
# refer to initial wallet as w1
91+
w1 = nodes[0].get_wallet_rpc('')
92+
93+
# import private key and fund it
94+
address = addr.pop()
95+
privkey = w1.dumpprivkey(address)
96+
res = w2.importmulti([{'scriptPubKey': {'address': address}, 'keys': [privkey], 'timestamp': 'now'}])
97+
assert_equal(res[0]['success'], True)
98+
w1.walletpassphrase('test', 100)
99+
100+
res = w1.sendtoaddress(address=address, amount=0.00010000)
101+
nodes[0].generate(1)
102+
destination = addr.pop()
103+
104+
# Using a fee rate (10 sat / byte) well above the minimum relay rate
105+
# creating a 5,000 sat transaction with change should not be possible
106+
assert_raises_rpc_error(-4, "Transaction needs a change address, but we can't generate it. Please call keypoolrefill first.", w2.walletcreatefundedpsbt, inputs=[], outputs=[{addr.pop(): 0.00005000}], options={"subtractFeeFromOutputs": [0], "feeRate": 0.00010})
107+
108+
# creating a 10,000 sat transaction without change, with a manual input, should still be possible
109+
res = w2.walletcreatefundedpsbt(inputs=w2.listunspent(), outputs=[{destination: 0.00010000}], options={"subtractFeeFromOutputs": [0], "feeRate": 0.00010})
110+
assert_equal("psbt" in res, True)
111+
112+
# creating a 10,000 sat transaction without change should still be possible
113+
res = w2.walletcreatefundedpsbt(inputs=[], outputs=[{destination: 0.00010000}], options={"subtractFeeFromOutputs": [0], "feeRate": 0.00010})
114+
assert_equal("psbt" in res, True)
115+
# should work without subtractFeeFromOutputs if the exact fee is subtracted from the amount
116+
res = w2.walletcreatefundedpsbt(inputs=[], outputs=[{destination: 0.00008900}], options={"feeRate": 0.00010})
117+
assert_equal("psbt" in res, True)
118+
119+
# dust change should be removed
120+
res = w2.walletcreatefundedpsbt(inputs=[], outputs=[{destination: 0.00008800}], options={"feeRate": 0.00010})
121+
assert_equal("psbt" in res, True)
122+
123+
# create a transaction without change at the maximum fee rate, such that the output is still spendable:
124+
res = w2.walletcreatefundedpsbt(inputs=[], outputs=[{destination: 0.00010000}], options={"subtractFeeFromOutputs": [0], "feeRate": 0.0008824})
125+
assert_equal("psbt" in res, True)
126+
assert_equal(res["fee"], Decimal("0.00009706"))
127+
128+
# creating a 10,000 sat transaction with a manual change address should be possible
129+
res = w2.walletcreatefundedpsbt(inputs=[], outputs=[{destination: 0.00010000}], options={"subtractFeeFromOutputs": [0], "feeRate": 0.00010, "changeAddress": addr.pop()})
130+
assert_equal("psbt" in res, True)
131+
132+
85133
if __name__ == '__main__':
86134
KeyPoolTest().main()

0 commit comments

Comments
 (0)