You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
375 lines
17 KiB
375 lines
17 KiB
#!/usr/bin/env python3
|
|
# Copyright (c) 2016-2019 The Bitcoin Core developers
|
|
# Distributed under the MIT software license, see the accompanying
|
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
"""Test the bumpfee RPC.
|
|
|
|
Verifies that the bumpfee RPC creates replacement transactions successfully when
|
|
its preconditions are met, and returns appropriate errors in other cases.
|
|
|
|
This module consists of around a dozen individual test cases implemented in the
|
|
top-level functions named as test_<test_case_description>. The test functions
|
|
can be disabled or reordered if needed for debugging. If new test cases are
|
|
added in the future, they should try to follow the same convention and not
|
|
make assumptions about execution order.
|
|
"""
|
|
from decimal import Decimal
|
|
import io
|
|
|
|
from test_framework.blocktools import add_witness_commitment, create_block, create_coinbase, send_to_witness
|
|
from test_framework.messages import BIP125_SEQUENCE_NUMBER, CTransaction
|
|
from test_framework.test_framework import BitcoinTestFramework
|
|
from test_framework.util import (
|
|
assert_equal,
|
|
assert_greater_than,
|
|
assert_raises_rpc_error,
|
|
connect_nodes_bi,
|
|
hex_str_to_bytes,
|
|
)
|
|
|
|
WALLET_PASSPHRASE = "test"
|
|
WALLET_PASSPHRASE_TIMEOUT = 3600
|
|
|
|
class BumpFeeTest(BitcoinTestFramework):
|
|
def set_test_params(self):
|
|
self.num_nodes = 2
|
|
self.setup_clean_chain = True
|
|
self.extra_args = [[
|
|
"-walletrbf={}".format(i),
|
|
"-mintxfee=0.00002",
|
|
] for i in range(self.num_nodes)]
|
|
|
|
def skip_test_if_missing_module(self):
|
|
self.skip_if_no_wallet()
|
|
|
|
def run_test(self):
|
|
# Encrypt wallet for test_locked_wallet_fails test
|
|
self.nodes[1].encryptwallet(WALLET_PASSPHRASE)
|
|
self.nodes[1].walletpassphrase(WALLET_PASSPHRASE, WALLET_PASSPHRASE_TIMEOUT)
|
|
|
|
connect_nodes_bi(self.nodes, 0, 1)
|
|
self.sync_all()
|
|
|
|
peer_node, rbf_node = self.nodes
|
|
rbf_node_address = rbf_node.getnewaddress()
|
|
|
|
# fund rbf node with 10 coins of 0.001 btc (100,000 satoshis)
|
|
self.log.info("Mining blocks...")
|
|
peer_node.generate(110)
|
|
self.sync_all()
|
|
for i in range(25):
|
|
peer_node.sendtoaddress(rbf_node_address, 0.001)
|
|
self.sync_all()
|
|
peer_node.generate(1)
|
|
self.sync_all()
|
|
assert_equal(rbf_node.getbalance(), Decimal("0.025"))
|
|
|
|
self.log.info("Running tests")
|
|
dest_address = peer_node.getnewaddress()
|
|
test_simple_bumpfee_succeeds(self, rbf_node, peer_node, dest_address)
|
|
test_segwit_bumpfee_succeeds(rbf_node, dest_address)
|
|
test_nonrbf_bumpfee_fails(peer_node, dest_address)
|
|
test_notmine_bumpfee_fails(rbf_node, peer_node, dest_address)
|
|
test_bumpfee_with_descendant_fails(rbf_node, rbf_node_address, dest_address)
|
|
test_small_output_fails(rbf_node, dest_address)
|
|
test_dust_to_fee(rbf_node, dest_address)
|
|
test_settxfee(rbf_node, dest_address)
|
|
test_rebumping(rbf_node, dest_address)
|
|
test_rebumping_not_replaceable(rbf_node, dest_address)
|
|
test_unconfirmed_not_spendable(rbf_node, rbf_node_address)
|
|
test_bumpfee_metadata(rbf_node, dest_address)
|
|
test_locked_wallet_fails(rbf_node, dest_address)
|
|
test_change_script_match(rbf_node, dest_address)
|
|
# These tests wipe out a number of utxos that are expected in other tests
|
|
test_small_output_with_feerate_succeeds(rbf_node, dest_address)
|
|
test_no_more_inputs_fails(rbf_node, dest_address)
|
|
self.log.info("Success")
|
|
|
|
|
|
def test_simple_bumpfee_succeeds(self, rbf_node, peer_node, dest_address):
|
|
rbfid = spend_one_input(rbf_node, dest_address)
|
|
rbftx = rbf_node.gettransaction(rbfid)
|
|
self.sync_mempools((rbf_node, peer_node))
|
|
assert rbfid in rbf_node.getrawmempool() and rbfid in peer_node.getrawmempool()
|
|
bumped_tx = rbf_node.bumpfee(rbfid)
|
|
assert_equal(bumped_tx["errors"], [])
|
|
assert bumped_tx["fee"] - abs(rbftx["fee"]) > 0
|
|
# check that bumped_tx propagates, original tx was evicted and has a wallet conflict
|
|
self.sync_mempools((rbf_node, peer_node))
|
|
assert bumped_tx["txid"] in rbf_node.getrawmempool()
|
|
assert bumped_tx["txid"] in peer_node.getrawmempool()
|
|
assert rbfid not in rbf_node.getrawmempool()
|
|
assert rbfid not in peer_node.getrawmempool()
|
|
oldwtx = rbf_node.gettransaction(rbfid)
|
|
assert len(oldwtx["walletconflicts"]) > 0
|
|
# check wallet transaction replaces and replaced_by values
|
|
bumpedwtx = rbf_node.gettransaction(bumped_tx["txid"])
|
|
assert_equal(oldwtx["replaced_by_txid"], bumped_tx["txid"])
|
|
assert_equal(bumpedwtx["replaces_txid"], rbfid)
|
|
|
|
|
|
def test_segwit_bumpfee_succeeds(rbf_node, dest_address):
|
|
# Create a transaction with segwit output, then create an RBF transaction
|
|
# which spends it, and make sure bumpfee can be called on it.
|
|
|
|
segwit_in = next(u for u in rbf_node.listunspent() if u["amount"] == Decimal("0.001"))
|
|
segwit_out = rbf_node.getaddressinfo(rbf_node.getnewaddress(address_type='p2sh-segwit'))
|
|
segwitid = send_to_witness(
|
|
use_p2wsh=False,
|
|
node=rbf_node,
|
|
utxo=segwit_in,
|
|
pubkey=segwit_out["pubkey"],
|
|
encode_p2sh=False,
|
|
amount=Decimal("0.0009"),
|
|
sign=True)
|
|
|
|
rbfraw = rbf_node.createrawtransaction([{
|
|
'txid': segwitid,
|
|
'vout': 0,
|
|
"sequence": BIP125_SEQUENCE_NUMBER
|
|
}], {dest_address: Decimal("0.0005"),
|
|
rbf_node.getrawchangeaddress(): Decimal("0.0003")})
|
|
rbfsigned = rbf_node.signrawtransactionwithwallet(rbfraw)
|
|
rbfid = rbf_node.sendrawtransaction(rbfsigned["hex"])
|
|
assert rbfid in rbf_node.getrawmempool()
|
|
|
|
bumped_tx = rbf_node.bumpfee(rbfid)
|
|
assert bumped_tx["txid"] in rbf_node.getrawmempool()
|
|
assert rbfid not in rbf_node.getrawmempool()
|
|
|
|
|
|
def test_nonrbf_bumpfee_fails(peer_node, dest_address):
|
|
# cannot replace a non RBF transaction (from node which did not enable RBF)
|
|
not_rbfid = peer_node.sendtoaddress(dest_address, Decimal("0.00090000"))
|
|
assert_raises_rpc_error(-4, "not BIP 125 replaceable", peer_node.bumpfee, not_rbfid)
|
|
|
|
|
|
def test_notmine_bumpfee_fails(rbf_node, peer_node, dest_address):
|
|
# cannot bump fee unless the tx has only inputs that we own.
|
|
# here, the rbftx has a peer_node coin and then adds a rbf_node input
|
|
# Note that this test depends upon the RPC code checking input ownership prior to change outputs
|
|
# (since it can't use fundrawtransaction, it lacks a proper change output)
|
|
utxos = [node.listunspent()[-1] for node in (rbf_node, peer_node)]
|
|
inputs = [{
|
|
"txid": utxo["txid"],
|
|
"vout": utxo["vout"],
|
|
"address": utxo["address"],
|
|
"sequence": BIP125_SEQUENCE_NUMBER
|
|
} for utxo in utxos]
|
|
output_val = sum(utxo["amount"] for utxo in utxos) - Decimal("0.001")
|
|
rawtx = rbf_node.createrawtransaction(inputs, {dest_address: output_val})
|
|
signedtx = rbf_node.signrawtransactionwithwallet(rawtx)
|
|
signedtx = peer_node.signrawtransactionwithwallet(signedtx["hex"])
|
|
rbfid = rbf_node.sendrawtransaction(signedtx["hex"])
|
|
assert_raises_rpc_error(-4, "Transaction contains inputs that don't belong to this wallet",
|
|
rbf_node.bumpfee, rbfid)
|
|
|
|
|
|
def test_bumpfee_with_descendant_fails(rbf_node, rbf_node_address, dest_address):
|
|
# cannot bump fee if the transaction has a descendant
|
|
# parent is send-to-self, so we don't have to check which output is change when creating the child tx
|
|
parent_id = spend_one_input(rbf_node, rbf_node_address)
|
|
tx = rbf_node.createrawtransaction([{"txid": parent_id, "vout": 0}], {dest_address: 0.00020000})
|
|
tx = rbf_node.signrawtransactionwithwallet(tx)
|
|
rbf_node.sendrawtransaction(tx["hex"])
|
|
assert_raises_rpc_error(-8, "Transaction has descendants in the wallet", rbf_node.bumpfee, parent_id)
|
|
|
|
|
|
def test_small_output_fails(rbf_node, dest_address):
|
|
# cannot bump fee with a too-small output
|
|
rbfid = spend_one_input(rbf_node, dest_address)
|
|
rbf_node.bumpfee(rbfid, {"totalFee": 50000})
|
|
|
|
rbfid = spend_one_input(rbf_node, dest_address)
|
|
assert_raises_rpc_error(-4, "Change output is too small", rbf_node.bumpfee, rbfid, {"totalFee": 50001})
|
|
|
|
def test_small_output_with_feerate_succeeds(rbf_node, dest_address):
|
|
|
|
# Make sure additional inputs exist
|
|
rbf_node.generatetoaddress(101, rbf_node.getnewaddress())
|
|
rbfid = spend_one_input(rbf_node, dest_address)
|
|
original_input_list = rbf_node.getrawtransaction(rbfid, 1)["vin"]
|
|
assert_equal(len(original_input_list), 1)
|
|
original_txin = original_input_list[0]
|
|
# Keep bumping until we out-spend change output
|
|
tx_fee = 0
|
|
while tx_fee < Decimal("0.0005"):
|
|
new_input_list = rbf_node.getrawtransaction(rbfid, 1)["vin"]
|
|
new_item = list(new_input_list)[0]
|
|
assert_equal(len(original_input_list), 1)
|
|
assert_equal(original_txin["txid"], new_item["txid"])
|
|
assert_equal(original_txin["vout"], new_item["vout"])
|
|
rbfid_new_details = rbf_node.bumpfee(rbfid)
|
|
rbfid_new = rbfid_new_details["txid"]
|
|
raw_pool = rbf_node.getrawmempool()
|
|
assert rbfid not in raw_pool
|
|
assert rbfid_new in raw_pool
|
|
rbfid = rbfid_new
|
|
tx_fee = rbfid_new_details["origfee"]
|
|
|
|
# input(s) have been added
|
|
final_input_list = rbf_node.getrawtransaction(rbfid, 1)["vin"]
|
|
assert_greater_than(len(final_input_list), 1)
|
|
# Original input is in final set
|
|
assert [txin for txin in final_input_list
|
|
if txin["txid"] == original_txin["txid"]
|
|
and txin["vout"] == original_txin["vout"]]
|
|
|
|
rbf_node.generatetoaddress(1, rbf_node.getnewaddress())
|
|
assert_equal(rbf_node.gettransaction(rbfid)["confirmations"], 1)
|
|
|
|
def test_dust_to_fee(rbf_node, dest_address):
|
|
# check that if output is reduced to dust, it will be converted to fee
|
|
# the bumped tx sets fee=49,900, but it converts to 50,000
|
|
rbfid = spend_one_input(rbf_node, dest_address)
|
|
fulltx = rbf_node.getrawtransaction(rbfid, 1)
|
|
# (32-byte p2sh-pwpkh output size + 148 p2pkh spend estimate) * 10k(discard_rate) / 1000 = 1800
|
|
# P2SH outputs are slightly "over-discarding" due to the IsDust calculation assuming it will
|
|
# be spent as a P2PKH.
|
|
bumped_tx = rbf_node.bumpfee(rbfid, {"totalFee": 50000 - 1800})
|
|
full_bumped_tx = rbf_node.getrawtransaction(bumped_tx["txid"], 1)
|
|
assert_equal(bumped_tx["fee"], Decimal("0.00050000"))
|
|
assert_equal(len(fulltx["vout"]), 2)
|
|
assert_equal(len(full_bumped_tx["vout"]), 1) # change output is eliminated
|
|
|
|
|
|
def test_settxfee(rbf_node, dest_address):
|
|
assert_raises_rpc_error(-8, "txfee cannot be less than min relay tx fee", rbf_node.settxfee, Decimal('0.000005'))
|
|
assert_raises_rpc_error(-8, "txfee cannot be less than wallet min fee", rbf_node.settxfee, Decimal('0.000015'))
|
|
# check that bumpfee reacts correctly to the use of settxfee (paytxfee)
|
|
rbfid = spend_one_input(rbf_node, dest_address)
|
|
requested_feerate = Decimal("0.00025000")
|
|
rbf_node.settxfee(requested_feerate)
|
|
bumped_tx = rbf_node.bumpfee(rbfid)
|
|
actual_feerate = bumped_tx["fee"] * 1000 / rbf_node.getrawtransaction(bumped_tx["txid"], True)["vsize"]
|
|
# Assert that the difference between the requested feerate and the actual
|
|
# feerate of the bumped transaction is small.
|
|
assert_greater_than(Decimal("0.00001000"), abs(requested_feerate - actual_feerate))
|
|
rbf_node.settxfee(Decimal("0.00000000")) # unset paytxfee
|
|
|
|
|
|
def test_rebumping(rbf_node, dest_address):
|
|
# check that re-bumping the original tx fails, but bumping the bumper succeeds
|
|
rbfid = spend_one_input(rbf_node, dest_address)
|
|
bumped = rbf_node.bumpfee(rbfid, {"totalFee": 2000})
|
|
assert_raises_rpc_error(-4, "already bumped", rbf_node.bumpfee, rbfid, {"totalFee": 3000})
|
|
rbf_node.bumpfee(bumped["txid"], {"totalFee": 3000})
|
|
|
|
|
|
def test_rebumping_not_replaceable(rbf_node, dest_address):
|
|
# check that re-bumping a non-replaceable bump tx fails
|
|
rbfid = spend_one_input(rbf_node, dest_address)
|
|
bumped = rbf_node.bumpfee(rbfid, {"totalFee": 10000, "replaceable": False})
|
|
assert_raises_rpc_error(-4, "Transaction is not BIP 125 replaceable", rbf_node.bumpfee, bumped["txid"],
|
|
{"totalFee": 20000})
|
|
|
|
|
|
def test_unconfirmed_not_spendable(rbf_node, rbf_node_address):
|
|
# check that unconfirmed outputs from bumped transactions are not spendable
|
|
rbfid = spend_one_input(rbf_node, rbf_node_address)
|
|
rbftx = rbf_node.gettransaction(rbfid)["hex"]
|
|
assert rbfid in rbf_node.getrawmempool()
|
|
bumpid = rbf_node.bumpfee(rbfid)["txid"]
|
|
assert bumpid in rbf_node.getrawmempool()
|
|
assert rbfid not in rbf_node.getrawmempool()
|
|
|
|
# check that outputs from the bump transaction are not spendable
|
|
# due to the replaces_txid check in CWallet::AvailableCoins
|
|
assert_equal([t for t in rbf_node.listunspent(minconf=0, include_unsafe=False) if t["txid"] == bumpid], [])
|
|
|
|
# submit a block with the rbf tx to clear the bump tx out of the mempool,
|
|
# then invalidate the block so the rbf tx will be put back in the mempool.
|
|
# This makes it possible to check whether the rbf tx outputs are
|
|
# spendable before the rbf tx is confirmed.
|
|
block = submit_block_with_tx(rbf_node, rbftx)
|
|
# Can not abandon conflicted tx
|
|
assert_raises_rpc_error(-5, 'Transaction not eligible for abandonment', lambda: rbf_node.abandontransaction(txid=bumpid))
|
|
rbf_node.invalidateblock(block.hash)
|
|
# Call abandon to make sure the wallet doesn't attempt to resubmit
|
|
# the bump tx and hope the wallet does not rebroadcast before we call.
|
|
rbf_node.abandontransaction(bumpid)
|
|
assert bumpid not in rbf_node.getrawmempool()
|
|
assert rbfid in rbf_node.getrawmempool()
|
|
|
|
# check that outputs from the rbf tx are not spendable before the
|
|
# transaction is confirmed, due to the replaced_by_txid check in
|
|
# CWallet::AvailableCoins
|
|
assert_equal([t for t in rbf_node.listunspent(minconf=0, include_unsafe=False) if t["txid"] == rbfid], [])
|
|
|
|
# check that the main output from the rbf tx is spendable after confirmed
|
|
rbf_node.generate(1)
|
|
assert_equal(
|
|
sum(1 for t in rbf_node.listunspent(minconf=0, include_unsafe=False)
|
|
if t["txid"] == rbfid and t["address"] == rbf_node_address and t["spendable"]), 1)
|
|
|
|
|
|
def test_bumpfee_metadata(rbf_node, dest_address):
|
|
rbfid = rbf_node.sendtoaddress(dest_address, Decimal("0.00100000"), "comment value", "to value")
|
|
bumped_tx = rbf_node.bumpfee(rbfid)
|
|
bumped_wtx = rbf_node.gettransaction(bumped_tx["txid"])
|
|
assert_equal(bumped_wtx["comment"], "comment value")
|
|
assert_equal(bumped_wtx["to"], "to value")
|
|
|
|
|
|
def test_locked_wallet_fails(rbf_node, dest_address):
|
|
rbfid = spend_one_input(rbf_node, dest_address)
|
|
rbf_node.walletlock()
|
|
assert_raises_rpc_error(-13, "Please enter the wallet passphrase with walletpassphrase first.",
|
|
rbf_node.bumpfee, rbfid)
|
|
rbf_node.walletpassphrase(WALLET_PASSPHRASE, WALLET_PASSPHRASE_TIMEOUT)
|
|
|
|
def test_change_script_match(rbf_node, dest_address):
|
|
"""Test that the same change addresses is used for the replacement transaction when possible."""
|
|
def get_change_address(tx):
|
|
tx_details = rbf_node.getrawtransaction(tx, 1)
|
|
txout_addresses = [txout['scriptPubKey']['addresses'][0] for txout in tx_details["vout"]]
|
|
return [address for address in txout_addresses if rbf_node.getaddressinfo(address)["ischange"]]
|
|
|
|
# Check that there is only one change output
|
|
rbfid = spend_one_input(rbf_node, dest_address)
|
|
change_addresses = get_change_address(rbfid)
|
|
assert_equal(len(change_addresses), 1)
|
|
|
|
# Now find that address in each subsequent tx, and no other change
|
|
bumped_total_tx = rbf_node.bumpfee(rbfid, {"totalFee": 2000})
|
|
assert_equal(change_addresses, get_change_address(bumped_total_tx['txid']))
|
|
bumped_rate_tx = rbf_node.bumpfee(bumped_total_tx["txid"])
|
|
assert_equal(change_addresses, get_change_address(bumped_rate_tx['txid']))
|
|
|
|
def spend_one_input(node, dest_address, change_size=Decimal("0.00049000")):
|
|
tx_input = dict(
|
|
sequence=BIP125_SEQUENCE_NUMBER, **next(u for u in node.listunspent() if u["amount"] == Decimal("0.00100000")))
|
|
destinations = {dest_address: Decimal("0.00050000")}
|
|
if change_size > 0:
|
|
destinations[node.getrawchangeaddress()] = change_size
|
|
rawtx = node.createrawtransaction([tx_input], destinations)
|
|
signedtx = node.signrawtransactionwithwallet(rawtx)
|
|
txid = node.sendrawtransaction(signedtx["hex"])
|
|
return txid
|
|
|
|
def submit_block_with_tx(node, tx):
|
|
ctx = CTransaction()
|
|
ctx.deserialize(io.BytesIO(hex_str_to_bytes(tx)))
|
|
|
|
tip = node.getbestblockhash()
|
|
height = node.getblockcount() + 1
|
|
block_time = node.getblockheader(tip)["mediantime"] + 1
|
|
block = create_block(int(tip, 16), create_coinbase(height), block_time)
|
|
block.vtx.append(ctx)
|
|
block.rehash()
|
|
block.hashMerkleRoot = block.calc_merkle_root()
|
|
add_witness_commitment(block)
|
|
block.solve()
|
|
node.submitblock(block.serialize(True).hex())
|
|
return block
|
|
|
|
def test_no_more_inputs_fails(rbf_node, dest_address):
|
|
# feerate rbf requires confirmed outputs when change output doesn't exist or is insufficient
|
|
rbf_node.generatetoaddress(1, dest_address)
|
|
# spend all funds, no change output
|
|
rbfid = rbf_node.sendtoaddress(rbf_node.getnewaddress(), rbf_node.getbalance(), "", "", True)
|
|
assert_raises_rpc_error(-4, "Unable to create transaction: Insufficient funds", rbf_node.bumpfee, rbfid)
|
|
|
|
if __name__ == "__main__":
|
|
BumpFeeTest().main()
|