Skip to content

Commit ceab670

Browse files
committed
bumpfee: Special case for reducing single output tx
When choosing the only output of a single output tx as the output to reduce the fee from, we need to deal with it specially as the normal transcation creation code cannot handle the fact that there are no specified recipients.
1 parent f1ccc08 commit ceab670

File tree

2 files changed

+72
-19
lines changed

2 files changed

+72
-19
lines changed

src/wallet/feebumper.cpp

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -247,16 +247,17 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo
247247

248248
old_fee = input_value - output_value;
249249

250+
// We need to make a temporary transaction with no input witnesses as the dummy signer expects them to be empty for external inputs
251+
CMutableTransaction temp_mtx{*wtx.tx};
252+
for (auto& txin : temp_mtx.vin) {
253+
txin.scriptSig.clear();
254+
txin.scriptWitness.SetNull();
255+
}
256+
const int64_t maxTxSize{CalculateMaximumSignedTxSize(CTransaction(temp_mtx), &wallet, &new_coin_control).vsize};
257+
250258
if (coin_control.m_feerate) {
251259
// The user provided a feeRate argument.
252260
// We calculate this here to avoid compiler warning on the cs_wallet lock
253-
// We need to make a temporary transaction with no input witnesses as the dummy signer expects them to be empty for external inputs
254-
CMutableTransaction mtx{*wtx.tx};
255-
for (auto& txin : mtx.vin) {
256-
txin.scriptSig.clear();
257-
txin.scriptWitness.SetNull();
258-
}
259-
const int64_t maxTxSize{CalculateMaximumSignedTxSize(CTransaction(mtx), &wallet, &new_coin_control).vsize};
260261
Result res = CheckFeeRate(wallet, wtx, *new_coin_control.m_feerate, maxTxSize, old_fee, errors);
261262
if (res != Result::OK) {
262263
return res;
@@ -281,21 +282,35 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo
281282
// We cannot source new unconfirmed inputs(bip125 rule 2)
282283
new_coin_control.m_min_depth = 1;
283284

284-
constexpr int RANDOM_CHANGE_POSITION = -1;
285-
auto res = CreateTransaction(wallet, recipients, RANDOM_CHANGE_POSITION, new_coin_control, false);
286-
if (!res) {
287-
errors.push_back(Untranslated("Unable to create transaction.") + Untranslated(" ") + util::ErrorString(res));
288-
return Result::WALLET_ERROR;
289-
}
285+
if (recipients.size() > 0) {
286+
constexpr int RANDOM_CHANGE_POSITION = -1;
287+
auto res = CreateTransaction(wallet, recipients, RANDOM_CHANGE_POSITION, new_coin_control, false);
288+
if (!res) {
289+
errors.push_back(Untranslated("Unable to create transaction.") + Untranslated(" ") + util::ErrorString(res));
290+
return Result::WALLET_ERROR;
291+
}
290292

291-
const auto& txr = *res;
292-
// Write back new fee if successful
293-
new_fee = txr.fee;
293+
const auto& txr = *res;
294+
// Write back new fee if successful
295+
new_fee = txr.fee;
294296

295-
// Write back transaction
296-
mtx = CMutableTransaction(*txr.tx);
297+
// Write back transaction
298+
mtx = CMutableTransaction(*txr.tx);
297299

298-
return Result::OK;
300+
return Result::OK;
301+
} else {
302+
// The transaction only has one output, and it is either detected as change,
303+
// or selected as the one to take the fee from.
304+
// In that case, just reduce its value to meet the new feerate.
305+
new_fee = new_coin_control.m_feerate->GetFee(maxTxSize);
306+
temp_mtx.vout[0].nValue += old_fee - new_fee;
307+
if (IsDust(temp_mtx.vout[0], wallet.chain().relayDustFee())) {
308+
errors.push_back(Untranslated("Single output amount too small"));
309+
return Result::MISC_ERROR;
310+
}
311+
mtx = temp_mtx;
312+
return Result::OK;
313+
}
299314
}
300315

301316
bool SignTransaction(CWallet& wallet, CMutableTransaction& mtx) {

test/functional/wallet_bumpfee.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from test_framework.test_framework import BitcoinTestFramework
2929
from test_framework.util import (
3030
assert_equal,
31+
assert_fee_amount,
3132
assert_greater_than,
3233
assert_raises_rpc_error,
3334
find_vout_for_address,
@@ -105,6 +106,7 @@ def run_test(self):
105106
test_small_output_with_feerate_succeeds(self, rbf_node, dest_address)
106107
test_no_more_inputs_fails(self, rbf_node, dest_address)
107108
self.test_provided_change_pos(rbf_node)
109+
self.test_single_output()
108110

109111
def test_invalid_parameters(self, rbf_node, peer_node, dest_address):
110112
self.log.info('Test invalid parameters')
@@ -195,6 +197,42 @@ def test_provided_change_pos(self, rbf_node):
195197
assert_greater_than(change_value, new_change_value)
196198

197199

200+
def test_single_output(self):
201+
self.log.info("Test that single output txs can be bumped")
202+
node = self.nodes[1]
203+
204+
node.createwallet("single_out_rbf")
205+
wallet = node.get_wallet_rpc("single_out_rbf")
206+
207+
addr = wallet.getnewaddress()
208+
amount = Decimal("0.001")
209+
# Make 2 UTXOs
210+
self.nodes[0].sendtoaddress(addr, amount)
211+
self.nodes[0].sendtoaddress(addr, amount)
212+
self.generate(self.nodes[0], 1)
213+
utxos = wallet.listunspent()
214+
215+
tx = wallet.sendall(recipients=[wallet.getnewaddress()], fee_rate=2, options={"inputs": [utxos[0]]})
216+
217+
# Reduce the only output with a crazy high feerate, should fail as the output would be dust
218+
assert_raises_rpc_error(-1, "Single output amount too small", wallet.bumpfee, txid=tx["txid"], options={"fee_rate": 1100, "reduce_output": 0})
219+
220+
# Reduce the only output successfully
221+
bumped = wallet.bumpfee(txid=tx["txid"], options={"fee_rate": 10, "reduce_output": 0})
222+
bumped_tx = wallet.gettransaction(txid=bumped["txid"], verbose=True)
223+
assert_equal(len(bumped_tx["decoded"]["vout"]), 1)
224+
assert_equal(len(bumped_tx["decoded"]["vin"]), 1)
225+
assert_equal(bumped_tx["decoded"]["vout"][0]["value"] + bumped["fee"], amount)
226+
assert_fee_amount(bumped["fee"], bumped_tx["decoded"]["vsize"], Decimal(10) / Decimal(1e8) * 1000)
227+
228+
# Bumping without reducing adds a new input and output
229+
bumped = wallet.bumpfee(txid=bumped["txid"], options={"fee_rate": 20})
230+
bumped_tx = wallet.gettransaction(txid=bumped["txid"], verbose=True)
231+
assert_equal(len(bumped_tx["decoded"]["vout"]), 2)
232+
assert_equal(len(bumped_tx["decoded"]["vin"]), 2)
233+
assert_fee_amount(bumped["fee"], bumped_tx["decoded"]["vsize"], Decimal(20) / Decimal(1e8) * 1000)
234+
235+
198236
def test_simple_bumpfee_succeeds(self, mode, rbf_node, peer_node, dest_address):
199237
self.log.info('Test simple bumpfee: {}'.format(mode))
200238
rbfid = spend_one_input(rbf_node, dest_address)

0 commit comments

Comments
 (0)