|
| 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() |
0 commit comments