Skip to content

Commit 2ef52d3

Browse files
committed
Merge #8456: [RPC] Simplified bumpfee command.
cc0243a [RPC] bumpfee (mrbandrews) 52dde66 [wallet] Add include_unsafe argument to listunspent RPC (Russell Yanofsky) 766e8a4 [wallet] Add IsAllFromMe: true if all inputs are from wallet (Suhas Daftuar)
2 parents 054d664 + cc0243a commit 2ef52d3

File tree

8 files changed

+715
-14
lines changed

8 files changed

+715
-14
lines changed

qa/pull-tester/rpc-tests.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@
151151
'signmessages.py',
152152
'nulldummy.py',
153153
'import-rescan.py',
154+
'bumpfee.py',
154155
'rpcnamedargs.py',
155156
]
156157
if ENABLE_ZMQ:

qa/rpc-tests/bumpfee.py

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2016 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
6+
from segwit import send_to_witness
7+
from test_framework.test_framework import BitcoinTestFramework
8+
from test_framework import blocktools
9+
from test_framework.mininode import CTransaction
10+
from test_framework.util import *
11+
from test_framework.util import *
12+
13+
import io
14+
import time
15+
16+
# Sequence number that is BIP 125 opt-in and BIP 68-compliant
17+
BIP125_SEQUENCE_NUMBER = 0xfffffffd
18+
19+
WALLET_PASSPHRASE = "test"
20+
WALLET_PASSPHRASE_TIMEOUT = 3600
21+
22+
23+
class BumpFeeTest(BitcoinTestFramework):
24+
def __init__(self):
25+
super().__init__()
26+
self.num_nodes = 2
27+
self.setup_clean_chain = True
28+
29+
def setup_network(self, split=False):
30+
extra_args = [["-debug", "-prematurewitness", "-walletprematurewitness", "-walletrbf={}".format(i)]
31+
for i in range(self.num_nodes)]
32+
self.nodes = start_nodes(self.num_nodes, self.options.tmpdir, extra_args)
33+
34+
# Encrypt wallet for test_locked_wallet_fails test
35+
self.nodes[1].encryptwallet(WALLET_PASSPHRASE)
36+
bitcoind_processes[1].wait()
37+
self.nodes[1] = start_node(1, self.options.tmpdir, extra_args[1])
38+
self.nodes[1].walletpassphrase(WALLET_PASSPHRASE, WALLET_PASSPHRASE_TIMEOUT)
39+
40+
connect_nodes_bi(self.nodes, 0, 1)
41+
self.is_network_split = False
42+
self.sync_all()
43+
44+
def run_test(self):
45+
peer_node, rbf_node = self.nodes
46+
rbf_node_address = rbf_node.getnewaddress()
47+
48+
# fund rbf node with 10 coins of 0.001 btc (100,000 satoshis)
49+
print("Mining blocks...")
50+
peer_node.generate(110)
51+
self.sync_all()
52+
for i in range(25):
53+
peer_node.sendtoaddress(rbf_node_address, 0.001)
54+
self.sync_all()
55+
peer_node.generate(1)
56+
self.sync_all()
57+
assert_equal(rbf_node.getbalance(), Decimal("0.025"))
58+
59+
print("Running tests")
60+
dest_address = peer_node.getnewaddress()
61+
test_small_output_fails(rbf_node, dest_address)
62+
test_dust_to_fee(rbf_node, dest_address)
63+
test_simple_bumpfee_succeeds(rbf_node, peer_node, dest_address)
64+
test_segwit_bumpfee_succeeds(rbf_node, dest_address)
65+
test_nonrbf_bumpfee_fails(peer_node, dest_address)
66+
test_notmine_bumpfee_fails(rbf_node, peer_node, dest_address)
67+
test_bumpfee_with_descendant_fails(rbf_node, rbf_node_address, dest_address)
68+
test_settxfee(rbf_node, dest_address)
69+
test_rebumping(rbf_node, dest_address)
70+
test_rebumping_not_replaceable(rbf_node, dest_address)
71+
test_unconfirmed_not_spendable(rbf_node, rbf_node_address)
72+
test_locked_wallet_fails(rbf_node, dest_address)
73+
print("Success")
74+
75+
76+
def test_simple_bumpfee_succeeds(rbf_node, peer_node, dest_address):
77+
rbfid = create_fund_sign_send(rbf_node, {dest_address: 0.00090000})
78+
rbftx = rbf_node.gettransaction(rbfid)
79+
sync_mempools((rbf_node, peer_node))
80+
assert rbfid in rbf_node.getrawmempool() and rbfid in peer_node.getrawmempool()
81+
bumped_tx = rbf_node.bumpfee(rbfid)
82+
assert bumped_tx["fee"] - abs(rbftx["fee"]) > 0
83+
# check that bumped_tx propogates, original tx was evicted and has a wallet conflict
84+
sync_mempools((rbf_node, peer_node))
85+
assert bumped_tx["txid"] in rbf_node.getrawmempool()
86+
assert bumped_tx["txid"] in peer_node.getrawmempool()
87+
assert rbfid not in rbf_node.getrawmempool()
88+
assert rbfid not in peer_node.getrawmempool()
89+
oldwtx = rbf_node.gettransaction(rbfid)
90+
assert len(oldwtx["walletconflicts"]) > 0
91+
# check wallet transaction replaces and replaced_by values
92+
bumpedwtx = rbf_node.gettransaction(bumped_tx["txid"])
93+
assert_equal(oldwtx["replaced_by_txid"], bumped_tx["txid"])
94+
assert_equal(bumpedwtx["replaces_txid"], rbfid)
95+
96+
97+
def test_segwit_bumpfee_succeeds(rbf_node, dest_address):
98+
# Create a transaction with segwit output, then create an RBF transaction
99+
# which spends it, and make sure bumpfee can be called on it.
100+
101+
segwit_in = next(u for u in rbf_node.listunspent() if u["amount"] == Decimal("0.001"))
102+
segwit_out = rbf_node.validateaddress(rbf_node.getnewaddress())
103+
rbf_node.addwitnessaddress(segwit_out["address"])
104+
segwitid = send_to_witness(
105+
version=0,
106+
node=rbf_node,
107+
utxo=segwit_in,
108+
pubkey=segwit_out["pubkey"],
109+
encode_p2sh=False,
110+
amount=Decimal("0.0009"),
111+
sign=True)
112+
113+
rbfraw = rbf_node.createrawtransaction([{
114+
'txid': segwitid,
115+
'vout': 0,
116+
"sequence": BIP125_SEQUENCE_NUMBER
117+
}], {dest_address: Decimal("0.0005"),
118+
get_change_address(rbf_node): Decimal("0.0003")})
119+
rbfsigned = rbf_node.signrawtransaction(rbfraw)
120+
rbfid = rbf_node.sendrawtransaction(rbfsigned["hex"])
121+
assert rbfid in rbf_node.getrawmempool()
122+
123+
bumped_tx = rbf_node.bumpfee(rbfid)
124+
assert bumped_tx["txid"] in rbf_node.getrawmempool()
125+
assert rbfid not in rbf_node.getrawmempool()
126+
127+
128+
def test_nonrbf_bumpfee_fails(peer_node, dest_address):
129+
# cannot replace a non RBF transaction (from node which did not enable RBF)
130+
not_rbfid = create_fund_sign_send(peer_node, {dest_address: 0.00090000})
131+
assert_raises_message(JSONRPCException, "not BIP 125 replaceable", peer_node.bumpfee, not_rbfid)
132+
133+
134+
def test_notmine_bumpfee_fails(rbf_node, peer_node, dest_address):
135+
# cannot bump fee unless the tx has only inputs that we own.
136+
# here, the rbftx has a peer_node coin and then adds a rbf_node input
137+
# Note that this test depends upon the RPC code checking input ownership prior to change outputs
138+
# (since it can't use fundrawtransaction, it lacks a proper change output)
139+
utxos = [node.listunspent()[-1] for node in (rbf_node, peer_node)]
140+
inputs = [{
141+
"txid": utxo["txid"],
142+
"vout": utxo["vout"],
143+
"address": utxo["address"],
144+
"sequence": BIP125_SEQUENCE_NUMBER
145+
} for utxo in utxos]
146+
output_val = sum(utxo["amount"] for utxo in utxos) - Decimal("0.001")
147+
rawtx = rbf_node.createrawtransaction(inputs, {dest_address: output_val})
148+
signedtx = rbf_node.signrawtransaction(rawtx)
149+
signedtx = peer_node.signrawtransaction(signedtx["hex"])
150+
rbfid = rbf_node.sendrawtransaction(signedtx["hex"])
151+
assert_raises_message(JSONRPCException, "Transaction contains inputs that don't belong to this wallet",
152+
rbf_node.bumpfee, rbfid)
153+
154+
155+
def test_bumpfee_with_descendant_fails(rbf_node, rbf_node_address, dest_address):
156+
# cannot bump fee if the transaction has a descendant
157+
# parent is send-to-self, so we don't have to check which output is change when creating the child tx
158+
parent_id = create_fund_sign_send(rbf_node, {rbf_node_address: 0.00050000})
159+
tx = rbf_node.createrawtransaction([{"txid": parent_id, "vout": 0}], {dest_address: 0.00020000})
160+
tx = rbf_node.signrawtransaction(tx)
161+
txid = rbf_node.sendrawtransaction(tx["hex"])
162+
assert_raises_message(JSONRPCException, "Transaction has descendants in the wallet", rbf_node.bumpfee, parent_id)
163+
164+
165+
def test_small_output_fails(rbf_node, dest_address):
166+
# cannot bump fee with a too-small output
167+
rbfid = spend_one_input(rbf_node,
168+
Decimal("0.00100000"),
169+
{dest_address: 0.00080000,
170+
get_change_address(rbf_node): Decimal("0.00010000")})
171+
rbf_node.bumpfee(rbfid, {"totalFee": 20000})
172+
173+
rbfid = spend_one_input(rbf_node,
174+
Decimal("0.00100000"),
175+
{dest_address: 0.00080000,
176+
get_change_address(rbf_node): Decimal("0.00010000")})
177+
assert_raises_message(JSONRPCException, "Change output is too small", rbf_node.bumpfee, rbfid, {"totalFee": 20001})
178+
179+
180+
def test_dust_to_fee(rbf_node, dest_address):
181+
# check that if output is reduced to dust, it will be converted to fee
182+
# the bumped tx sets fee=9900, but it converts to 10,000
183+
rbfid = spend_one_input(rbf_node,
184+
Decimal("0.00100000"),
185+
{dest_address: 0.00080000,
186+
get_change_address(rbf_node): Decimal("0.00010000")})
187+
fulltx = rbf_node.getrawtransaction(rbfid, 1)
188+
bumped_tx = rbf_node.bumpfee(rbfid, {"totalFee": 19900})
189+
full_bumped_tx = rbf_node.getrawtransaction(bumped_tx["txid"], 1)
190+
assert_equal(bumped_tx["fee"], Decimal("0.00020000"))
191+
assert_equal(len(fulltx["vout"]), 2)
192+
assert_equal(len(full_bumped_tx["vout"]), 1) #change output is eliminated
193+
194+
195+
def test_settxfee(rbf_node, dest_address):
196+
# check that bumpfee reacts correctly to the use of settxfee (paytxfee)
197+
# increase feerate by 2.5x, test that fee increased at least 2x
198+
rbf_node.settxfee(Decimal("0.00001000"))
199+
rbfid = create_fund_sign_send(rbf_node, {dest_address: 0.00090000})
200+
rbftx = rbf_node.gettransaction(rbfid)
201+
rbf_node.settxfee(Decimal("0.00002500"))
202+
bumped_tx = rbf_node.bumpfee(rbfid)
203+
assert bumped_tx["fee"] > 2 * abs(rbftx["fee"])
204+
rbf_node.settxfee(Decimal("0.00000000")) # unset paytxfee
205+
206+
207+
def test_rebumping(rbf_node, dest_address):
208+
# check that re-bumping the original tx fails, but bumping the bumper succeeds
209+
rbf_node.settxfee(Decimal("0.00001000"))
210+
rbfid = create_fund_sign_send(rbf_node, {dest_address: 0.00090000})
211+
bumped = rbf_node.bumpfee(rbfid, {"totalFee": 1000})
212+
assert_raises_message(JSONRPCException, "already bumped", rbf_node.bumpfee, rbfid, {"totalFee": 2000})
213+
rbf_node.bumpfee(bumped["txid"], {"totalFee": 2000})
214+
215+
216+
def test_rebumping_not_replaceable(rbf_node, dest_address):
217+
# check that re-bumping a non-replaceable bump tx fails
218+
rbfid = create_fund_sign_send(rbf_node, {dest_address: 0.00090000})
219+
bumped = rbf_node.bumpfee(rbfid, {"totalFee": 10000, "replaceable": False})
220+
assert_raises_message(JSONRPCException, "Transaction is not BIP 125 replaceable", rbf_node.bumpfee, bumped["txid"],
221+
{"totalFee": 20000})
222+
223+
224+
def test_unconfirmed_not_spendable(rbf_node, rbf_node_address):
225+
# check that unconfirmed outputs from bumped transactions are not spendable
226+
rbfid = create_fund_sign_send(rbf_node, {rbf_node_address: 0.00090000})
227+
rbftx = rbf_node.gettransaction(rbfid)["hex"]
228+
assert rbfid in rbf_node.getrawmempool()
229+
bumpid = rbf_node.bumpfee(rbfid)["txid"]
230+
assert bumpid in rbf_node.getrawmempool()
231+
assert rbfid not in rbf_node.getrawmempool()
232+
233+
# check that outputs from the bump transaction are not spendable
234+
# due to the replaces_txid check in CWallet::AvailableCoins
235+
assert_equal([t for t in rbf_node.listunspent(minconf=0, include_unsafe=False) if t["txid"] == bumpid], [])
236+
237+
# submit a block with the rbf tx to clear the bump tx out of the mempool,
238+
# then call abandon to make sure the wallet doesn't attempt to resubmit the
239+
# bump tx, then invalidate the block so the rbf tx will be put back in the
240+
# mempool. this makes it possible to check whether the rbf tx outputs are
241+
# spendable before the rbf tx is confirmed.
242+
block = submit_block_with_tx(rbf_node, rbftx)
243+
rbf_node.abandontransaction(bumpid)
244+
rbf_node.invalidateblock(block.hash)
245+
assert bumpid not in rbf_node.getrawmempool()
246+
assert rbfid in rbf_node.getrawmempool()
247+
248+
# check that outputs from the rbf tx are not spendable before the
249+
# transaction is confirmed, due to the replaced_by_txid check in
250+
# CWallet::AvailableCoins
251+
assert_equal([t for t in rbf_node.listunspent(minconf=0, include_unsafe=False) if t["txid"] == rbfid], [])
252+
253+
# check that the main output from the rbf tx is spendable after confirmed
254+
rbf_node.generate(1)
255+
assert_equal(
256+
sum(1 for t in rbf_node.listunspent(minconf=0, include_unsafe=False)
257+
if t["txid"] == rbfid and t["address"] == rbf_node_address and t["spendable"]), 1)
258+
259+
260+
def test_locked_wallet_fails(rbf_node, dest_address):
261+
rbfid = create_fund_sign_send(rbf_node, {dest_address: 0.00090000})
262+
rbf_node.walletlock()
263+
assert_raises_message(JSONRPCException, "Please enter the wallet passphrase with walletpassphrase first.",
264+
rbf_node.bumpfee, rbfid)
265+
266+
267+
def create_fund_sign_send(node, outputs):
268+
rawtx = node.createrawtransaction([], outputs)
269+
fundtx = node.fundrawtransaction(rawtx)
270+
signedtx = node.signrawtransaction(fundtx["hex"])
271+
txid = node.sendrawtransaction(signedtx["hex"])
272+
return txid
273+
274+
275+
def spend_one_input(node, input_amount, outputs):
276+
input = dict(sequence=BIP125_SEQUENCE_NUMBER, **next(u for u in node.listunspent() if u["amount"] == input_amount))
277+
rawtx = node.createrawtransaction([input], outputs)
278+
signedtx = node.signrawtransaction(rawtx)
279+
txid = node.sendrawtransaction(signedtx["hex"])
280+
return txid
281+
282+
283+
def get_change_address(node):
284+
"""Get a wallet change address.
285+
286+
There is no wallet RPC to access unused change addresses, so this creates a
287+
dummy transaction, calls fundrawtransaction to give add an input and change
288+
output, then returns the change address."""
289+
dest_address = node.getnewaddress()
290+
dest_amount = Decimal("0.00012345")
291+
rawtx = node.createrawtransaction([], {dest_address: dest_amount})
292+
fundtx = node.fundrawtransaction(rawtx)
293+
info = node.decoderawtransaction(fundtx["hex"])
294+
return next(address for out in info["vout"]
295+
if out["value"] != dest_amount for address in out["scriptPubKey"]["addresses"])
296+
297+
298+
def submit_block_with_tx(node, tx):
299+
ctx = CTransaction()
300+
ctx.deserialize(io.BytesIO(hex_str_to_bytes(tx)))
301+
302+
tip = node.getbestblockhash()
303+
height = node.getblockcount() + 1
304+
block_time = node.getblockheader(tip)["mediantime"] + 1
305+
block = blocktools.create_block(int(tip, 16), blocktools.create_coinbase(height), block_time)
306+
block.vtx.append(ctx)
307+
block.rehash()
308+
block.hashMerkleRoot = block.calc_merkle_root()
309+
block.solve()
310+
error = node.submitblock(bytes_to_hex_str(block.serialize(True)))
311+
if error is not None:
312+
raise Exception(error)
313+
return block
314+
315+
316+
if __name__ == "__main__":
317+
BumpFeeTest().main()

src/rpc/client.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
117117
{ "setnetworkactive", 0, "state" },
118118
{ "getmempoolancestors", 1, "verbose" },
119119
{ "getmempooldescendants", 1, "verbose" },
120+
{ "bumpfee", 1, "options" },
120121
// Echo with conversion (For testing only)
121122
{ "echojson", 0, "arg0" },
122123
{ "echojson", 1, "arg1" },

src/rpc/server.cpp

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,16 +79,20 @@ void RPCTypeCheck(const UniValue& params,
7979
break;
8080

8181
const UniValue& v = params[i];
82-
if (!((v.type() == t) || (fAllowNull && (v.isNull()))))
83-
{
84-
string err = strprintf("Expected type %s, got %s",
85-
uvTypeName(t), uvTypeName(v.type()));
86-
throw JSONRPCError(RPC_TYPE_ERROR, err);
82+
if (!(fAllowNull && v.isNull())) {
83+
RPCTypeCheckArgument(v, t);
8784
}
8885
i++;
8986
}
9087
}
9188

89+
void RPCTypeCheckArgument(const UniValue& value, UniValue::VType typeExpected)
90+
{
91+
if (value.type() != typeExpected) {
92+
throw JSONRPCError(RPC_TYPE_ERROR, strprintf("Expected type %s, got %s", uvTypeName(typeExpected), uvTypeName(value.type())));
93+
}
94+
}
95+
9296
void RPCTypeCheckObj(const UniValue& o,
9397
const map<string, UniValueType>& typesExpected,
9498
bool fAllowNull,

src/rpc/server.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ bool RPCIsInWarmup(std::string *statusOut);
7878
void RPCTypeCheck(const UniValue& params,
7979
const std::list<UniValue::VType>& typesExpected, bool fAllowNull=false);
8080

81+
/**
82+
* Type-check one argument; throws JSONRPCError if wrong type given.
83+
*/
84+
void RPCTypeCheckArgument(const UniValue& value, UniValue::VType typeExpected);
85+
8186
/*
8287
Check for expected keys/value types in an Object.
8388
*/

0 commit comments

Comments
 (0)