Skip to content

Commit 3497df4

Browse files
committed
Merge bitcoin/bitcoin#27195: bumpfee: allow send coins back to yourself
be72663 test: bumpfee, add coverage for "send coins back to yourself" (furszy) 7bffec6 bumpfee: enable send coins back to yourself (furszy) Pull request description: Simple example: 1) User_1 sends 0.1 btc to user_2 on a low fee transaction. 2) After few hours, the tx is still in the mempool, user_2 is not interested anymore, so user_1 decides to cancel it by sending coins back to himself. 3) User_1 has the bright idea of opening the explorer and copy the change output address of the transaction. Then call bumpfee providing such output (in the "outputs" arg). Currently, this is not possible. The wallet fails with "Unable to create transaction. Transaction must have at least one recipient" error. The error reason is because we discard the provided output from the recipients list and set it inside the coin control so the process adds it later (when the change is calculated). But.. there is no later if the tx has no outputs. ACKs for top commit: ishaanam: reACK be72663 achow101: ACK be72663 Tree-SHA512: c2c38290a998f9b426a830d9624c7feb730158980ac186f8fb0138d5e200935d6538307bc60a2c3d0b7b6ee2b4ffb77a1e98baf8feb1d20a7d825f6055ac377f
2 parents 0713088 + be72663 commit 3497df4

File tree

2 files changed

+72
-8
lines changed

2 files changed

+72
-8
lines changed

src/wallet/feebumper.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo
231231
// is one). If outputs vector is non-empty, replace original
232232
// outputs with its contents, otherwise use original outputs.
233233
std::vector<CRecipient> recipients;
234+
CAmount new_outputs_value = 0;
234235
const auto& txouts = outputs.empty() ? wtx.tx->vout : outputs;
235236
for (const auto& output : txouts) {
236237
if (!OutputIsChange(wallet, output)) {
@@ -241,6 +242,21 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo
241242
ExtractDestination(output.scriptPubKey, change_dest);
242243
new_coin_control.destChange = change_dest;
243244
}
245+
new_outputs_value += output.nValue;
246+
}
247+
248+
// If no recipients, means that we are sending coins to a change address
249+
if (recipients.empty()) {
250+
// Just as a sanity check, ensure that the change address exist
251+
if (std::get_if<CNoDestination>(&new_coin_control.destChange)) {
252+
errors.emplace_back(Untranslated("Unable to create transaction. Transaction must have at least one recipient"));
253+
return Result::INVALID_PARAMETER;
254+
}
255+
256+
// Add change as recipient with SFFO flag enabled, so fees are deduced from it.
257+
// If the output differs from the original tx output (because the user customized it) a new change output will be created.
258+
recipients.emplace_back(CRecipient{GetScriptForDestination(new_coin_control.destChange), new_outputs_value, /*fSubtractFeeFromAmount=*/true});
259+
new_coin_control.destChange = CNoDestination();
244260
}
245261

246262
if (coin_control.m_feerate) {

test/functional/wallet_bumpfee.py

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@
4141
HIGH = 500
4242
TOO_HIGH = 100000
4343

44+
def get_change_address(tx, node):
45+
tx_details = node.getrawtransaction(tx, 1)
46+
txout_addresses = [txout['scriptPubKey']['address'] for txout in tx_details["vout"]]
47+
return [address for address in txout_addresses if node.getaddressinfo(address)["ischange"]]
4448

4549
class BumpFeeTest(BitcoinTestFramework):
4650
def add_options(self, parser):
@@ -104,6 +108,7 @@ def run_test(self):
104108
# These tests wipe out a number of utxos that are expected in other tests
105109
test_small_output_with_feerate_succeeds(self, rbf_node, dest_address)
106110
test_no_more_inputs_fails(self, rbf_node, dest_address)
111+
self.test_bump_back_to_yourself()
107112

108113
# Context independent tests
109114
test_feerate_checks_replaced_outputs(self, rbf_node, peer_node)
@@ -171,6 +176,54 @@ def test_invalid_parameters(self, rbf_node, peer_node, dest_address):
171176

172177
self.clear_mempool()
173178

179+
def test_bump_back_to_yourself(self):
180+
self.log.info("Test that bumpfee can send coins back to yourself")
181+
node = self.nodes[1]
182+
183+
node.createwallet("back_to_yourself")
184+
wallet = node.get_wallet_rpc("back_to_yourself")
185+
186+
# Make 3 UTXOs
187+
addr = wallet.getnewaddress()
188+
for _ in range(3):
189+
self.nodes[0].sendtoaddress(addr, 5)
190+
self.generate(self.nodes[0], 1)
191+
192+
# Create a tx with two outputs. recipient and change.
193+
tx = wallet.send(outputs={wallet.getnewaddress(): 9}, fee_rate=2)
194+
tx_info = wallet.gettransaction(txid=tx["txid"], verbose=True)
195+
assert_equal(len(tx_info["decoded"]["vout"]), 2)
196+
assert_equal(len(tx_info["decoded"]["vin"]), 2)
197+
198+
# Bump tx, send coins back to change address.
199+
change_addr = get_change_address(tx["txid"], wallet)[0]
200+
out_amount = 10
201+
bumped = wallet.bumpfee(txid=tx["txid"], options={"fee_rate": 20, "outputs": [{change_addr: out_amount}]})
202+
bumped_tx = wallet.gettransaction(txid=bumped["txid"], verbose=True)
203+
assert_equal(len(bumped_tx["decoded"]["vout"]), 1)
204+
assert_equal(len(bumped_tx["decoded"]["vin"]), 2)
205+
assert_equal(bumped_tx["decoded"]["vout"][0]["value"] + bumped["fee"], out_amount)
206+
207+
# Bump tx again, now test send fewer coins back to change address.
208+
out_amount = 6
209+
bumped = wallet.bumpfee(txid=bumped["txid"], options={"fee_rate": 40, "outputs": [{change_addr: out_amount}]})
210+
bumped_tx = wallet.gettransaction(txid=bumped["txid"], verbose=True)
211+
assert_equal(len(bumped_tx["decoded"]["vout"]), 2)
212+
assert_equal(len(bumped_tx["decoded"]["vin"]), 2)
213+
assert any(txout['value'] == out_amount - bumped["fee"] and txout['scriptPubKey']['address'] == change_addr for txout in bumped_tx['decoded']['vout'])
214+
# Check that total out amount is still equal to the previously bumped tx
215+
assert_equal(bumped_tx["decoded"]["vout"][0]["value"] + bumped_tx["decoded"]["vout"][1]["value"] + bumped["fee"], 10)
216+
217+
# Bump tx again, send more coins back to change address. The process will add another input to cover the target.
218+
out_amount = 12
219+
bumped = wallet.bumpfee(txid=bumped["txid"], options={"fee_rate": 80, "outputs": [{change_addr: out_amount}]})
220+
bumped_tx = wallet.gettransaction(txid=bumped["txid"], verbose=True)
221+
assert_equal(len(bumped_tx["decoded"]["vout"]), 2)
222+
assert_equal(len(bumped_tx["decoded"]["vin"]), 3)
223+
assert any(txout['value'] == out_amount - bumped["fee"] and txout['scriptPubKey']['address'] == change_addr for txout in bumped_tx['decoded']['vout'])
224+
assert_equal(bumped_tx["decoded"]["vout"][0]["value"] + bumped_tx["decoded"]["vout"][1]["value"] + bumped["fee"], 15)
225+
226+
node.unloadwallet("back_to_yourself")
174227

175228
def test_simple_bumpfee_succeeds(self, mode, rbf_node, peer_node, dest_address):
176229
self.log.info('Test simple bumpfee: {}'.format(mode))
@@ -635,21 +688,16 @@ def test_locked_wallet_fails(self, rbf_node, dest_address):
635688
def test_change_script_match(self, rbf_node, dest_address):
636689
self.log.info('Test that the same change addresses is used for the replacement transaction when possible')
637690

638-
def get_change_address(tx):
639-
tx_details = rbf_node.getrawtransaction(tx, 1)
640-
txout_addresses = [txout['scriptPubKey']['address'] for txout in tx_details["vout"]]
641-
return [address for address in txout_addresses if rbf_node.getaddressinfo(address)["ischange"]]
642-
643691
# Check that there is only one change output
644692
rbfid = spend_one_input(rbf_node, dest_address)
645-
change_addresses = get_change_address(rbfid)
693+
change_addresses = get_change_address(rbfid, rbf_node)
646694
assert_equal(len(change_addresses), 1)
647695

648696
# Now find that address in each subsequent tx, and no other change
649697
bumped_total_tx = rbf_node.bumpfee(rbfid, {"fee_rate": ECONOMICAL})
650-
assert_equal(change_addresses, get_change_address(bumped_total_tx['txid']))
698+
assert_equal(change_addresses, get_change_address(bumped_total_tx['txid'], rbf_node))
651699
bumped_rate_tx = rbf_node.bumpfee(bumped_total_tx["txid"])
652-
assert_equal(change_addresses, get_change_address(bumped_rate_tx['txid']))
700+
assert_equal(change_addresses, get_change_address(bumped_rate_tx['txid'], rbf_node))
653701
self.clear_mempool()
654702

655703

0 commit comments

Comments
 (0)