aboutsummaryrefslogtreecommitdiff
path: root/test/functional
diff options
context:
space:
mode:
Diffstat (limited to 'test/functional')
-rwxr-xr-xtest/functional/feature_config_args.py94
-rwxr-xr-xtest/functional/feature_signet.py3
-rwxr-xr-xtest/functional/interface_usdt_mempool.py4
-rwxr-xr-xtest/functional/mempool_compatibility.py2
-rwxr-xr-xtest/functional/mempool_package_limits.py3
-rwxr-xr-xtest/functional/mempool_packages.py15
-rwxr-xr-xtest/functional/mempool_persist.py1
-rwxr-xr-xtest/functional/p2p_compactblocks.py88
-rwxr-xr-xtest/functional/rpc_getblockfrompeer.py4
-rwxr-xr-xtest/functional/rpc_invalid_address_message.py6
-rwxr-xr-xtest/functional/rpc_validateaddress.py203
-rwxr-xr-xtest/functional/test_framework/test_node.py4
-rw-r--r--test/functional/test_framework/util.py20
-rw-r--r--test/functional/test_framework/wallet.py7
-rwxr-xr-xtest/functional/test_runner.py3
-rwxr-xr-xtest/functional/wallet_abandonconflict.py12
-rwxr-xr-xtest/functional/wallet_basic.py33
-rwxr-xr-xtest/functional/wallet_conflicts.py127
18 files changed, 591 insertions, 38 deletions
diff --git a/test/functional/feature_config_args.py b/test/functional/feature_config_args.py
index f9730b48c5..2257605870 100755
--- a/test/functional/feature_config_args.py
+++ b/test/functional/feature_config_args.py
@@ -5,9 +5,14 @@
"""Test various command line arguments and configuration file parameters."""
import os
+import pathlib
+import re
+import sys
+import tempfile
import time
from test_framework.test_framework import BitcoinTestFramework
+from test_framework.test_node import ErrorMatch
from test_framework import util
@@ -74,7 +79,7 @@ class ConfArgsTest(BitcoinTestFramework):
util.write_config(main_conf_file_path, n=0, chain='', extra_config=f'includeconf={inc_conf_file_path}\n')
with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
conf.write('acceptnonstdtxn=1\n')
- self.nodes[0].assert_start_raises_init_error(extra_args=[f"-conf={main_conf_file_path}"], expected_msg='Error: acceptnonstdtxn is not currently supported for main chain')
+ self.nodes[0].assert_start_raises_init_error(extra_args=[f"-conf={main_conf_file_path}", "-allowignoredconf"], expected_msg='Error: acceptnonstdtxn is not currently supported for main chain')
with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
conf.write('nono\n')
@@ -108,6 +113,41 @@ class ConfArgsTest(BitcoinTestFramework):
with open(inc_conf_file2_path, 'w', encoding='utf-8') as conf:
conf.write('') # clear
+ def test_config_file_log(self):
+ # Disable this test for windows currently because trying to override
+ # the default datadir through the environment does not seem to work.
+ if sys.platform == "win32":
+ return
+
+ self.log.info('Test that correct configuration path is changed when configuration file changes the datadir')
+
+ # Create a temporary directory that will be treated as the default data
+ # directory by bitcoind.
+ env, default_datadir = util.get_temp_default_datadir(pathlib.Path(self.options.tmpdir, "test_config_file_log"))
+ default_datadir.mkdir(parents=True)
+
+ # Write a bitcoin.conf file in the default data directory containing a
+ # datadir= line pointing at the node datadir.
+ node = self.nodes[0]
+ conf_text = pathlib.Path(node.bitcoinconf).read_text()
+ conf_path = default_datadir / "bitcoin.conf"
+ conf_path.write_text(f"datadir={node.datadir}\n{conf_text}")
+
+ # Drop the node -datadir= argument during this test, because if it is
+ # specified it would take precedence over the datadir setting in the
+ # config file.
+ node_args = node.args
+ node.args = [arg for arg in node.args if not arg.startswith("-datadir=")]
+
+ # Check that correct configuration file path is actually logged
+ # (conf_path, not node.bitcoinconf)
+ with self.nodes[0].assert_debug_log(expected_msgs=[f"Config file: {conf_path}"]):
+ self.start_node(0, ["-allowignoredconf"], env=env)
+ self.stop_node(0)
+
+ # Restore node arguments after the test
+ node.args = node_args
+
def test_invalid_command_line_options(self):
self.nodes[0].assert_start_raises_init_error(
expected_msg='Error: Error parsing command line arguments: Can not set -proxy with no value. Please specify value with -proxy=value.',
@@ -282,6 +322,55 @@ class ConfArgsTest(BitcoinTestFramework):
unexpected_msgs=seednode_ignored):
self.restart_node(0, extra_args=[connect_arg, '-seednode=fakeaddress2'])
+ def test_ignored_conf(self):
+ self.log.info('Test error is triggered when the datadir in use contains a bitcoin.conf file that would be ignored '
+ 'because a conflicting -conf file argument is passed.')
+ node = self.nodes[0]
+ with tempfile.NamedTemporaryFile(dir=self.options.tmpdir, mode="wt", delete=False) as temp_conf:
+ temp_conf.write(f"datadir={node.datadir}\n")
+ node.assert_start_raises_init_error([f"-conf={temp_conf.name}"], re.escape(
+ f'Error: Data directory "{node.datadir}" contains a "bitcoin.conf" file which is ignored, because a '
+ f'different configuration file "{temp_conf.name}" from command line argument "-conf={temp_conf.name}" '
+ f'is being used instead.') + r"[\s\S]*", match=ErrorMatch.FULL_REGEX)
+
+ # Test that passing a redundant -conf command line argument pointing to
+ # the same bitcoin.conf that would be loaded anyway does not trigger an
+ # error.
+ self.start_node(0, [f'-conf={node.datadir}/bitcoin.conf'])
+ self.stop_node(0)
+
+ def test_ignored_default_conf(self):
+ # Disable this test for windows currently because trying to override
+ # the default datadir through the environment does not seem to work.
+ if sys.platform == "win32":
+ return
+
+ self.log.info('Test error is triggered when bitcoin.conf in the default data directory sets another datadir '
+ 'and it contains a different bitcoin.conf file that would be ignored')
+
+ # Create a temporary directory that will be treated as the default data
+ # directory by bitcoind.
+ env, default_datadir = util.get_temp_default_datadir(pathlib.Path(self.options.tmpdir, "home"))
+ default_datadir.mkdir(parents=True)
+
+ # Write a bitcoin.conf file in the default data directory containing a
+ # datadir= line pointing at the node datadir. This will trigger a
+ # startup error because the node datadir contains a different
+ # bitcoin.conf that would be ignored.
+ node = self.nodes[0]
+ (default_datadir / "bitcoin.conf").write_text(f"datadir={node.datadir}\n")
+
+ # Drop the node -datadir= argument during this test, because if it is
+ # specified it would take precedence over the datadir setting in the
+ # config file.
+ node_args = node.args
+ node.args = [arg for arg in node.args if not arg.startswith("-datadir=")]
+ node.assert_start_raises_init_error([], re.escape(
+ f'Error: Data directory "{node.datadir}" contains a "bitcoin.conf" file which is ignored, because a '
+ f'different configuration file "{default_datadir}/bitcoin.conf" from data directory "{default_datadir}" '
+ f'is being used instead.') + r"[\s\S]*", env=env, match=ErrorMatch.FULL_REGEX)
+ node.args = node_args
+
def run_test(self):
self.test_log_buffer()
self.test_args_log()
@@ -290,7 +379,10 @@ class ConfArgsTest(BitcoinTestFramework):
self.test_connect_with_seednode()
self.test_config_file_parser()
+ self.test_config_file_log()
self.test_invalid_command_line_options()
+ self.test_ignored_conf()
+ self.test_ignored_default_conf()
# Remove the -datadir argument so it doesn't override the config file
self.nodes[0].args = [arg for arg in self.nodes[0].args if not arg.startswith("-datadir")]
diff --git a/test/functional/feature_signet.py b/test/functional/feature_signet.py
index b41fe378af..a90a2a8e5e 100755
--- a/test/functional/feature_signet.py
+++ b/test/functional/feature_signet.py
@@ -76,6 +76,9 @@ class SignetBasicTest(BitcoinTestFramework):
self.log.info("test that signet logs the network magic on node start")
with self.nodes[0].assert_debug_log(["Signet derived magic (message start)"]):
self.restart_node(0)
+ self.stop_node(0)
+ self.nodes[0].assert_start_raises_init_error(extra_args=["-signetchallenge=abc"], expected_msg="Error: -signetchallenge must be hex, not 'abc'.")
+ self.nodes[0].assert_start_raises_init_error(extra_args=["-signetchallenge=abc"] * 2, expected_msg="Error: -signetchallenge cannot be multiple values.")
if __name__ == '__main__':
diff --git a/test/functional/interface_usdt_mempool.py b/test/functional/interface_usdt_mempool.py
index ec2f9e4e77..542849d558 100755
--- a/test/functional/interface_usdt_mempool.py
+++ b/test/functional/interface_usdt_mempool.py
@@ -173,6 +173,7 @@ class MempoolTracepointTest(BitcoinTestFramework):
self.log.info("Ensuring mempool:added event was handled successfully...")
assert_equal(EXPECTED_ADDED_EVENTS, handled_added_events)
+ self.generate(self.wallet, 1)
def removed_test(self):
"""Expire a transaction from the mempool and make sure the tracepoint returns
@@ -223,6 +224,7 @@ class MempoolTracepointTest(BitcoinTestFramework):
self.log.info("Ensuring mempool:removed event was handled successfully...")
assert_equal(EXPECTED_REMOVED_EVENTS, handled_removed_events)
+ self.generate(self.wallet, 1)
def replaced_test(self):
"""Replace one and two transactions in the mempool and make sure the tracepoint
@@ -280,6 +282,7 @@ class MempoolTracepointTest(BitcoinTestFramework):
self.log.info("Ensuring mempool:replaced event was handled successfully...")
assert_equal(EXPECTED_REPLACED_EVENTS, handled_replaced_events)
+ self.generate(self.wallet, 1)
def rejected_test(self):
"""Create an invalid transaction and make sure the tracepoint returns
@@ -321,6 +324,7 @@ class MempoolTracepointTest(BitcoinTestFramework):
self.log.info("Ensuring mempool:rejected event was handled successfully...")
assert_equal(EXPECTED_REJECTED_EVENTS, handled_rejected_events)
+ self.generate(self.wallet, 1)
def run_test(self):
"""Tests the mempool:added, mempool:removed, mempool:replaced,
diff --git a/test/functional/mempool_compatibility.py b/test/functional/mempool_compatibility.py
index a7bdc49695..7337802aea 100755
--- a/test/functional/mempool_compatibility.py
+++ b/test/functional/mempool_compatibility.py
@@ -47,12 +47,12 @@ class MempoolCompatibilityTest(BitcoinTestFramework):
# unbroadcasted_tx won't pass old_node's `MemPoolAccept::PreChecks`.
self.connect_nodes(0, 1)
self.sync_blocks()
- self.stop_node(1)
self.log.info("Add a transaction to mempool on old node and shutdown")
old_tx_hash = new_wallet.send_self_transfer(from_node=old_node)["txid"]
assert old_tx_hash in old_node.getrawmempool()
self.stop_node(0)
+ self.stop_node(1)
self.log.info("Move mempool.dat from old to new node")
old_node_mempool = os.path.join(old_node.datadir, self.chain, 'mempool.dat')
diff --git a/test/functional/mempool_package_limits.py b/test/functional/mempool_package_limits.py
index 6143ae83de..81451bf2a5 100755
--- a/test/functional/mempool_package_limits.py
+++ b/test/functional/mempool_package_limits.py
@@ -46,8 +46,7 @@ class MempoolPackageLimitsTest(BitcoinTestFramework):
def run_test(self):
self.wallet = MiniWallet(self.nodes[0])
# Add enough mature utxos to the wallet so that all txs spend confirmed coins.
- self.generate(self.wallet, 35)
- self.generate(self.nodes[0], COINBASE_MATURITY)
+ self.generate(self.wallet, COINBASE_MATURITY + 35)
self.test_chain_limits()
self.test_desc_count_limits()
diff --git a/test/functional/mempool_packages.py b/test/functional/mempool_packages.py
index 0387282862..95f7939412 100755
--- a/test/functional/mempool_packages.py
+++ b/test/functional/mempool_packages.py
@@ -7,7 +7,6 @@
from decimal import Decimal
from test_framework.messages import (
- COIN,
DEFAULT_ANCESTOR_LIMIT,
DEFAULT_DESCENDANT_LIMIT,
)
@@ -26,9 +25,6 @@ assert CUSTOM_DESCENDANT_LIMIT >= CUSTOM_ANCESTOR_LIMIT
class MempoolPackagesTest(BitcoinTestFramework):
- def add_options(self, parser):
- self.add_wallet_options(parser)
-
def set_test_params(self):
self.num_nodes = 2
self.extra_args = [
@@ -47,10 +43,6 @@ class MempoolPackagesTest(BitcoinTestFramework):
self.wallet = MiniWallet(self.nodes[0])
self.wallet.rescan_utxos()
- if self.is_specified_wallet_compiled():
- self.nodes[0].createwallet("watch_wallet", disable_private_keys=True)
- self.nodes[0].importaddress(self.wallet.get_address())
-
peer_inv_store = self.nodes[0].add_p2p_connection(P2PTxInvStore()) # keep track of invs
# DEFAULT_ANCESTOR_LIMIT transactions off a confirmed tx should be fine
@@ -63,13 +55,6 @@ class MempoolPackagesTest(BitcoinTestFramework):
ancestor_vsize += t["tx"].get_vsize()
ancestor_fees += t["fee"]
self.wallet.sendrawtransaction(from_node=self.nodes[0], tx_hex=t["hex"])
- # Check that listunspent ancestor{count, size, fees} yield the correct results
- if self.is_specified_wallet_compiled():
- wallet_unspent = self.nodes[0].listunspent(minconf=0)
- this_unspent = next(utxo_info for utxo_info in wallet_unspent if utxo_info["txid"] == t["txid"])
- assert_equal(this_unspent['ancestorcount'], i + 1)
- assert_equal(this_unspent['ancestorsize'], ancestor_vsize)
- assert_equal(this_unspent['ancestorfees'], ancestor_fees * COIN)
# Wait until mempool transactions have passed initial broadcast (sent inv and received getdata)
# Otherwise, getrawmempool may be inconsistent with getmempoolentry if unbroadcast changes in between
diff --git a/test/functional/mempool_persist.py b/test/functional/mempool_persist.py
index f818801136..8f74d9de20 100755
--- a/test/functional/mempool_persist.py
+++ b/test/functional/mempool_persist.py
@@ -191,6 +191,7 @@ class MempoolPersistTest(BitcoinTestFramework):
def test_persist_unbroadcast(self):
node0 = self.nodes[0]
self.start_node(0)
+ self.start_node(2)
# clear out mempool
self.generate(node0, 1, sync_fun=self.no_op)
diff --git a/test/functional/p2p_compactblocks.py b/test/functional/p2p_compactblocks.py
index 23eeea50bc..d6c06fdeed 100755
--- a/test/functional/p2p_compactblocks.py
+++ b/test/functional/p2p_compactblocks.py
@@ -105,6 +105,10 @@ class TestP2PConn(P2PInterface):
self.last_message.pop("headers", None)
self.last_message.pop("cmpctblock", None)
+ def clear_getblocktxn(self):
+ with p2p_lock:
+ self.last_message.pop("getblocktxn", None)
+
def get_headers(self, locator, hashstop):
msg = msg_getheaders()
msg.locator.vHave = locator
@@ -745,7 +749,7 @@ class CompactBlocksTest(BitcoinTestFramework):
peer.get_headers(locator=[int(tip, 16)], hashstop=0)
peer.send_and_ping(msg_sendcmpct(announce=True, version=2))
- def test_compactblock_reconstruction_multiple_peers(self, stalling_peer, delivery_peer):
+ def test_compactblock_reconstruction_stalling_peer(self, stalling_peer, delivery_peer):
node = self.nodes[0]
assert len(self.utxos)
@@ -823,12 +827,85 @@ class CompactBlocksTest(BitcoinTestFramework):
hb_test_node.send_and_ping(msg_sendcmpct(announce=False, version=2))
assert_highbandwidth_states(self.nodes[0], hb_to=True, hb_from=False)
+ def test_compactblock_reconstruction_parallel_reconstruction(self, stalling_peer, delivery_peer, inbound_peer, outbound_peer):
+ """ All p2p connections are inbound except outbound_peer. We test that ultimate parallel slot
+ can only be taken by an outbound node unless prior attempts were done by an outbound
+ """
+ node = self.nodes[0]
+ assert len(self.utxos)
+
+ def announce_cmpct_block(node, peer, txn_count):
+ utxo = self.utxos.pop(0)
+ block = self.build_block_with_transactions(node, utxo, txn_count)
+
+ cmpct_block = HeaderAndShortIDs()
+ cmpct_block.initialize_from_block(block)
+ msg = msg_cmpctblock(cmpct_block.to_p2p())
+ peer.send_and_ping(msg)
+ with p2p_lock:
+ assert "getblocktxn" in peer.last_message
+ return block, cmpct_block
+
+ for name, peer in [("delivery", delivery_peer), ("inbound", inbound_peer), ("outbound", outbound_peer)]:
+ self.log.info(f"Setting {name} as high bandwidth peer")
+ block, cmpct_block = announce_cmpct_block(node, peer, 1)
+ msg = msg_blocktxn()
+ msg.block_transactions.blockhash = block.sha256
+ msg.block_transactions.transactions = block.vtx[1:]
+ peer.send_and_ping(msg)
+ assert_equal(int(node.getbestblockhash(), 16), block.sha256)
+ peer.clear_getblocktxn()
+
+ # Test the simple parallel download case...
+ for num_missing in [1, 5, 20]:
+
+ # Remaining low-bandwidth peer is stalling_peer, who announces first
+ assert_equal([peer['bip152_hb_to'] for peer in node.getpeerinfo()], [False, True, True, True])
+
+ block, cmpct_block = announce_cmpct_block(node, stalling_peer, num_missing)
+
+ delivery_peer.send_and_ping(msg_cmpctblock(cmpct_block.to_p2p()))
+ with p2p_lock:
+ # The second peer to announce should still get a getblocktxn
+ assert "getblocktxn" in delivery_peer.last_message
+ assert int(node.getbestblockhash(), 16) != block.sha256
+
+ inbound_peer.send_and_ping(msg_cmpctblock(cmpct_block.to_p2p()))
+ with p2p_lock:
+ # The third inbound peer to announce should *not* get a getblocktxn
+ assert "getblocktxn" not in inbound_peer.last_message
+ assert int(node.getbestblockhash(), 16) != block.sha256
+
+ outbound_peer.send_and_ping(msg_cmpctblock(cmpct_block.to_p2p()))
+ with p2p_lock:
+ # The third peer to announce should get a getblocktxn if outbound
+ assert "getblocktxn" in outbound_peer.last_message
+ assert int(node.getbestblockhash(), 16) != block.sha256
+
+ # Second peer completes the compact block first
+ msg = msg_blocktxn()
+ msg.block_transactions.blockhash = block.sha256
+ msg.block_transactions.transactions = block.vtx[1:]
+ delivery_peer.send_and_ping(msg)
+ assert_equal(int(node.getbestblockhash(), 16), block.sha256)
+
+ # Nothing bad should happen if we get a late fill from the first peer...
+ stalling_peer.send_and_ping(msg)
+ self.utxos.append([block.vtx[-1].sha256, 0, block.vtx[-1].vout[0].nValue])
+
+ delivery_peer.clear_getblocktxn()
+ inbound_peer.clear_getblocktxn()
+ outbound_peer.clear_getblocktxn()
+
+
def run_test(self):
self.wallet = MiniWallet(self.nodes[0])
# Setup the p2p connections
self.segwit_node = self.nodes[0].add_p2p_connection(TestP2PConn())
self.additional_segwit_node = self.nodes[0].add_p2p_connection(TestP2PConn())
+ self.onemore_inbound_node = self.nodes[0].add_p2p_connection(TestP2PConn())
+ self.outbound_node = self.nodes[0].add_outbound_p2p_connection(TestP2PConn(), p2p_idx=3, connection_type="outbound-full-relay")
# We will need UTXOs to construct transactions in later tests.
self.make_utxos()
@@ -838,6 +915,8 @@ class CompactBlocksTest(BitcoinTestFramework):
self.log.info("Testing SENDCMPCT p2p message... ")
self.test_sendcmpct(self.segwit_node)
self.test_sendcmpct(self.additional_segwit_node)
+ self.test_sendcmpct(self.onemore_inbound_node)
+ self.test_sendcmpct(self.outbound_node)
self.log.info("Testing compactblock construction...")
self.test_compactblock_construction(self.segwit_node)
@@ -860,8 +939,11 @@ class CompactBlocksTest(BitcoinTestFramework):
self.log.info("Testing handling of incorrect blocktxn responses...")
self.test_incorrect_blocktxn_response(self.segwit_node)
- self.log.info("Testing reconstructing compact blocks from all peers...")
- self.test_compactblock_reconstruction_multiple_peers(self.segwit_node, self.additional_segwit_node)
+ self.log.info("Testing reconstructing compact blocks with a stalling peer...")
+ self.test_compactblock_reconstruction_stalling_peer(self.segwit_node, self.additional_segwit_node)
+
+ self.log.info("Testing reconstructing compact blocks from multiple peers...")
+ self.test_compactblock_reconstruction_parallel_reconstruction(stalling_peer=self.segwit_node, inbound_peer=self.onemore_inbound_node, delivery_peer=self.additional_segwit_node, outbound_peer=self.outbound_node)
# Test that if we submitblock to node1, we'll get a compact block
# announcement to all peers.
diff --git a/test/functional/rpc_getblockfrompeer.py b/test/functional/rpc_getblockfrompeer.py
index 2f093bebff..1ab1023cf1 100755
--- a/test/functional/rpc_getblockfrompeer.py
+++ b/test/functional/rpc_getblockfrompeer.py
@@ -117,9 +117,11 @@ class GetBlockFromPeerTest(BitcoinTestFramework):
assert_raises_rpc_error(-1, error_msg, self.nodes[1].getblockfrompeer, blockhash, node1_interface_id)
self.log.info("Connect pruned node")
- # We need to generate more blocks to be able to prune
self.connect_nodes(0, 2)
pruned_node = self.nodes[2]
+ self.sync_blocks([self.nodes[0], pruned_node])
+
+ # We need to generate more blocks to be able to prune
self.generate(self.nodes[0], 400, sync_fun=self.no_op)
self.sync_blocks([self.nodes[0], pruned_node])
pruneheight = pruned_node.pruneblockchain(300)
diff --git a/test/functional/rpc_invalid_address_message.py b/test/functional/rpc_invalid_address_message.py
index fd282a9bc1..6759b69dd1 100755
--- a/test/functional/rpc_invalid_address_message.py
+++ b/test/functional/rpc_invalid_address_message.py
@@ -63,12 +63,12 @@ class InvalidAddressErrorMessageTest(BitcoinTestFramework):
def test_validateaddress(self):
# Invalid Bech32
- self.check_invalid(BECH32_INVALID_SIZE, 'Invalid Bech32 address data size')
+ self.check_invalid(BECH32_INVALID_SIZE, "Invalid Bech32 address program size (41 bytes)")
self.check_invalid(BECH32_INVALID_PREFIX, 'Invalid or unsupported Segwit (Bech32) or Base58 encoding.')
self.check_invalid(BECH32_INVALID_BECH32, 'Version 1+ witness address must use Bech32m checksum')
self.check_invalid(BECH32_INVALID_BECH32M, 'Version 0 witness address must use Bech32 checksum')
self.check_invalid(BECH32_INVALID_VERSION, 'Invalid Bech32 address witness version')
- self.check_invalid(BECH32_INVALID_V0_SIZE, 'Invalid Bech32 v0 address data size')
+ self.check_invalid(BECH32_INVALID_V0_SIZE, "Invalid Bech32 v0 address program size (21 bytes), per BIP141")
self.check_invalid(BECH32_TOO_LONG, 'Bech32 string too long', list(range(90, 108)))
self.check_invalid(BECH32_ONE_ERROR, 'Invalid Bech32 checksum', [9])
self.check_invalid(BECH32_TWO_ERRORS, 'Invalid Bech32 checksum', [22, 43])
@@ -105,7 +105,7 @@ class InvalidAddressErrorMessageTest(BitcoinTestFramework):
def test_getaddressinfo(self):
node = self.nodes[0]
- assert_raises_rpc_error(-5, "Invalid Bech32 address data size", node.getaddressinfo, BECH32_INVALID_SIZE)
+ assert_raises_rpc_error(-5, "Invalid Bech32 address program size (41 bytes)", node.getaddressinfo, BECH32_INVALID_SIZE)
assert_raises_rpc_error(-5, "Invalid or unsupported Segwit (Bech32) or Base58 encoding.", node.getaddressinfo, BECH32_INVALID_PREFIX)
assert_raises_rpc_error(-5, "Invalid or unsupported Base58-encoded address.", node.getaddressinfo, BASE58_INVALID_PREFIX)
assert_raises_rpc_error(-5, "Invalid or unsupported Segwit (Bech32) or Base58 encoding.", node.getaddressinfo, INVALID_ADDRESS)
diff --git a/test/functional/rpc_validateaddress.py b/test/functional/rpc_validateaddress.py
new file mode 100755
index 0000000000..d87ba098c3
--- /dev/null
+++ b/test/functional/rpc_validateaddress.py
@@ -0,0 +1,203 @@
+#!/usr/bin/env python3
+# Copyright (c) 2023 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 validateaddress for main chain"""
+
+from test_framework.test_framework import BitcoinTestFramework
+
+from test_framework.util import assert_equal
+
+INVALID_DATA = [
+ # BIP 173
+ (
+ "tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty",
+ "Invalid or unsupported Segwit (Bech32) or Base58 encoding.", # Invalid hrp
+ [],
+ ),
+ ("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", "Invalid Bech32 checksum", [41]),
+ (
+ "BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2",
+ "Version 1+ witness address must use Bech32m checksum",
+ [],
+ ),
+ (
+ "bc1rw5uspcuh",
+ "Version 1+ witness address must use Bech32m checksum", # Invalid program length
+ [],
+ ),
+ (
+ "bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90",
+ "Version 1+ witness address must use Bech32m checksum", # Invalid program length
+ [],
+ ),
+ (
+ "BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P",
+ "Invalid Bech32 v0 address program size (16 bytes), per BIP141",
+ [],
+ ),
+ (
+ "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7",
+ "Invalid or unsupported Segwit (Bech32) or Base58 encoding.", # tb1, Mixed case
+ [],
+ ),
+ (
+ "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3t4",
+ "Invalid character or mixed case", # bc1, Mixed case, not in BIP 173 test vectors
+ [40],
+ ),
+ (
+ "bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du",
+ "Version 1+ witness address must use Bech32m checksum", # Wrong padding
+ [],
+ ),
+ (
+ "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv",
+ "Invalid or unsupported Segwit (Bech32) or Base58 encoding.", # tb1, Non-zero padding in 8-to-5 conversion
+ [],
+ ),
+ ("bc1gmk9yu", "Empty Bech32 data section", []),
+ # BIP 350
+ (
+ "tc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq5zuyut",
+ "Invalid or unsupported Segwit (Bech32) or Base58 encoding.", # Invalid human-readable part
+ [],
+ ),
+ (
+ "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqh2y7hd",
+ "Version 1+ witness address must use Bech32m checksum", # Invalid checksum (Bech32 instead of Bech32m)
+ [],
+ ),
+ (
+ "tb1z0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqglt7rf",
+ "Invalid or unsupported Segwit (Bech32) or Base58 encoding.", # tb1, Invalid checksum (Bech32 instead of Bech32m)
+ [],
+ ),
+ (
+ "BC1S0XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ54WELL",
+ "Version 1+ witness address must use Bech32m checksum", # Invalid checksum (Bech32 instead of Bech32m)
+ [],
+ ),
+ (
+ "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kemeawh",
+ "Version 0 witness address must use Bech32 checksum", # Invalid checksum (Bech32m instead of Bech32)
+ [],
+ ),
+ (
+ "tb1q0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq24jc47",
+ "Invalid or unsupported Segwit (Bech32) or Base58 encoding.", # tb1, Invalid checksum (Bech32m instead of Bech32)
+ [],
+ ),
+ (
+ "bc1p38j9r5y49hruaue7wxjce0updqjuyyx0kh56v8s25huc6995vvpql3jow4",
+ "Invalid Base 32 character", # Invalid character in checksum
+ [59],
+ ),
+ (
+ "BC130XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ7ZWS8R",
+ "Invalid Bech32 address witness version",
+ [],
+ ),
+ ("bc1pw5dgrnzv", "Invalid Bech32 address program size (1 byte)", []),
+ (
+ "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v8n0nx0muaewav253zgeav",
+ "Invalid Bech32 address program size (41 bytes)",
+ [],
+ ),
+ (
+ "BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P",
+ "Invalid Bech32 v0 address program size (16 bytes), per BIP141",
+ [],
+ ),
+ (
+ "tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq47Zagq",
+ "Invalid or unsupported Segwit (Bech32) or Base58 encoding.", # tb1, Mixed case
+ [],
+ ),
+ (
+ "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v07qwwzcrf",
+ "Invalid padding in Bech32 data section", # zero padding of more than 4 bits
+ [],
+ ),
+ (
+ "tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vpggkg4j",
+ "Invalid or unsupported Segwit (Bech32) or Base58 encoding.", # tb1, Non-zero padding in 8-to-5 conversion
+ [],
+ ),
+ ("bc1gmk9yu", "Empty Bech32 data section", []),
+]
+VALID_DATA = [
+ # BIP 350
+ (
+ "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4",
+ "0014751e76e8199196d454941c45d1b3a323f1433bd6",
+ ),
+ # (
+ # "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7",
+ # "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262",
+ # ),
+ (
+ "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3",
+ "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262",
+ ),
+ (
+ "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y",
+ "5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6",
+ ),
+ ("BC1SW50QGDZ25J", "6002751e"),
+ ("bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs", "5210751e76e8199196d454941c45d1b3a323"),
+ # (
+ # "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy",
+ # "0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433",
+ # ),
+ (
+ "bc1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvses5wp4dt",
+ "0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433",
+ ),
+ # (
+ # "tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c",
+ # "5120000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433",
+ # ),
+ (
+ "bc1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvses7epu4h",
+ "5120000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433",
+ ),
+ (
+ "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0",
+ "512079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ ),
+]
+
+
+class ValidateAddressMainTest(BitcoinTestFramework):
+ def set_test_params(self):
+ self.setup_clean_chain = True
+ self.chain = "" # main
+ self.num_nodes = 1
+ self.extra_args = [["-prune=899"]] * self.num_nodes
+
+ def check_valid(self, addr, spk):
+ info = self.nodes[0].validateaddress(addr)
+ assert_equal(info["isvalid"], True)
+ assert_equal(info["scriptPubKey"], spk)
+ assert "error" not in info
+ assert "error_locations" not in info
+
+ def check_invalid(self, addr, error_str, error_locations):
+ res = self.nodes[0].validateaddress(addr)
+ assert_equal(res["isvalid"], False)
+ assert_equal(res["error"], error_str)
+ assert_equal(res["error_locations"], error_locations)
+
+ def test_validateaddress(self):
+ for (addr, error, locs) in INVALID_DATA:
+ self.check_invalid(addr, error, locs)
+ for (addr, spk) in VALID_DATA:
+ self.check_valid(addr, spk)
+
+ def run_test(self):
+ self.test_validateaddress()
+
+
+if __name__ == "__main__":
+ ValidateAddressMainTest().main()
diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py
index 56abe5f26a..51bd697e81 100755
--- a/test/functional/test_framework/test_node.py
+++ b/test/functional/test_framework/test_node.py
@@ -190,7 +190,7 @@ class TestNode():
assert self.rpc_connected and self.rpc is not None, self._node_msg("Error: no RPC connection")
return getattr(RPCOverloadWrapper(self.rpc, descriptors=self.descriptors), name)
- def start(self, extra_args=None, *, cwd=None, stdout=None, stderr=None, **kwargs):
+ def start(self, extra_args=None, *, cwd=None, stdout=None, stderr=None, env=None, **kwargs):
"""Start the node."""
if extra_args is None:
extra_args = self.extra_args
@@ -213,6 +213,8 @@ class TestNode():
# add environment variable LIBC_FATAL_STDERR_=1 so that libc errors are written to stderr and not the terminal
subp_env = dict(os.environ, LIBC_FATAL_STDERR_="1")
+ if env is not None:
+ subp_env.update(env)
self.process = subprocess.Popen(self.args + extra_args, env=subp_env, stdout=stdout, stderr=stderr, cwd=cwd, **kwargs)
diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py
index 2c227922c5..d3b3e4d536 100644
--- a/test/functional/test_framework/util.py
+++ b/test/functional/test_framework/util.py
@@ -12,13 +12,15 @@ import inspect
import json
import logging
import os
+import pathlib
import random
import re
+import sys
import time
from . import coverage
from .authproxy import AuthServiceProxy, JSONRPCException
-from typing import Callable, Optional
+from typing import Callable, Optional, Tuple
logger = logging.getLogger("TestFramework.utils")
@@ -419,6 +421,22 @@ def get_datadir_path(dirname, n):
return os.path.join(dirname, "node" + str(n))
+def get_temp_default_datadir(temp_dir: pathlib.Path) -> Tuple[dict, pathlib.Path]:
+ """Return os-specific environment variables that can be set to make the
+ GetDefaultDataDir() function return a datadir path under the provided
+ temp_dir, as well as the complete path it would return."""
+ if sys.platform == "win32":
+ env = dict(APPDATA=str(temp_dir))
+ datadir = temp_dir / "Bitcoin"
+ else:
+ env = dict(HOME=str(temp_dir))
+ if sys.platform == "darwin":
+ datadir = temp_dir / "Library/Application Support/Bitcoin"
+ else:
+ datadir = temp_dir / ".bitcoin"
+ return env, datadir
+
+
def append_config(datadir, options):
with open(os.path.join(datadir, "bitcoin.conf"), 'a', encoding='utf8') as f:
for option in options:
diff --git a/test/functional/test_framework/wallet.py b/test/functional/test_framework/wallet.py
index 64606b818b..1d546e12bd 100644
--- a/test/functional/test_framework/wallet.py
+++ b/test/functional/test_framework/wallet.py
@@ -218,10 +218,12 @@ class MiniWallet:
txid: get the first utxo we find from a specific transaction
"""
self._utxos = sorted(self._utxos, key=lambda k: (k['value'], -k['height'])) # Put the largest utxo last
+ blocks_height = self._test_node.getblockchaininfo()['blocks']
+ mature_coins = list(filter(lambda utxo: not utxo['coinbase'] or COINBASE_MATURITY - 1 <= blocks_height - utxo['height'], self._utxos))
if txid:
utxo_filter: Any = filter(lambda utxo: txid == utxo['txid'], self._utxos)
else:
- utxo_filter = reversed(self._utxos) # By default the largest utxo
+ utxo_filter = reversed(mature_coins) # By default the largest utxo
if vout is not None:
utxo_filter = filter(lambda utxo: vout == utxo['vout'], utxo_filter)
index = self._utxos.index(next(utxo_filter))
@@ -233,7 +235,8 @@ class MiniWallet:
def get_utxos(self, *, include_immature_coinbase=False, mark_as_spent=True):
"""Returns the list of all utxos and optionally mark them as spent"""
if not include_immature_coinbase:
- utxo_filter = filter(lambda utxo: not utxo['coinbase'] or COINBASE_MATURITY <= utxo['confirmations'], self._utxos)
+ blocks_height = self._test_node.getblockchaininfo()['blocks']
+ utxo_filter = filter(lambda utxo: not utxo['coinbase'] or COINBASE_MATURITY - 1 <= blocks_height - utxo['height'], self._utxos)
else:
utxo_filter = self._utxos
utxos = deepcopy(list(utxo_filter))
diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py
index 3dc22ccde3..5895e1de84 100755
--- a/test/functional/test_runner.py
+++ b/test/functional/test_runner.py
@@ -171,6 +171,7 @@ BASE_SCRIPTS = [
'wallet_fast_rescan.py --descriptors',
'interface_zmq.py',
'rpc_invalid_address_message.py',
+ 'rpc_validateaddress.py',
'interface_bitcoin_cli.py --legacy-wallet',
'interface_bitcoin_cli.py --descriptors',
'feature_bind_extra.py',
@@ -195,6 +196,8 @@ BASE_SCRIPTS = [
'wallet_watchonly.py --legacy-wallet',
'wallet_watchonly.py --usecli --legacy-wallet',
'wallet_reorgsrestore.py',
+ 'wallet_conflicts.py --legacy-wallet',
+ 'wallet_conflicts.py --descriptors',
'interface_http.py',
'interface_rpc.py',
'interface_usdt_coinselection.py',
diff --git a/test/functional/wallet_abandonconflict.py b/test/functional/wallet_abandonconflict.py
index 934f44588d..2691507773 100755
--- a/test/functional/wallet_abandonconflict.py
+++ b/test/functional/wallet_abandonconflict.py
@@ -226,20 +226,16 @@ class AbandonConflictTest(BitcoinTestFramework):
assert_equal(double_spend["walletconflicts"], [txAB1])
# Verify that B and C's 10 BTC outputs are available for spending again because AB1 is now conflicted
+ assert_equal(alice.gettransaction(txAB1)["confirmations"], -1)
newbalance = alice.getbalance()
assert_equal(newbalance, balance + Decimal("20"))
balance = newbalance
- # There is currently a minor bug around this and so this test doesn't work. See Issue #7315
- # Invalidate the block with the double spend and B's 10 BTC output should no longer be available
- # Don't think C's should either
+ # Invalidate the block with the double spend. B & C's 10 BTC outputs should no longer be available
self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash())
+ assert_equal(alice.gettransaction(txAB1)["confirmations"], 0)
newbalance = alice.getbalance()
- #assert_equal(newbalance, balance - Decimal("10"))
- self.log.info("If balance has not declined after invalidateblock then out of mempool wallet tx which is no longer")
- self.log.info("conflicted has not resumed causing its inputs to be seen as spent. See Issue #7315")
- assert_equal(balance, newbalance)
-
+ assert_equal(newbalance, balance - Decimal("20"))
if __name__ == '__main__':
AbandonConflictTest().main()
diff --git a/test/functional/wallet_basic.py b/test/functional/wallet_basic.py
index e23ec7bc01..a1b805c09e 100755
--- a/test/functional/wallet_basic.py
+++ b/test/functional/wallet_basic.py
@@ -8,6 +8,10 @@ from itertools import product
from test_framework.blocktools import COINBASE_MATURITY
from test_framework.descriptors import descsum_create
+from test_framework.messages import (
+ COIN,
+ DEFAULT_ANCESTOR_LIMIT,
+)
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_array_result,
@@ -17,6 +21,7 @@ from test_framework.util import (
find_vout_for_address,
)
from test_framework.wallet_util import test_address
+from test_framework.wallet import MiniWallet
NOT_A_NUMBER_OR_STRING = "Amount is not a number or string"
OUT_OF_RANGE = "Amount out of range"
@@ -784,6 +789,34 @@ class WalletTest(BitcoinTestFramework):
zeroconf_wallet.sendtoaddress(zeroconf_wallet.getnewaddress(), Decimal('0.5'))
+ self.test_chain_listunspent()
+
+ def test_chain_listunspent(self):
+ if not self.options.descriptors:
+ return
+ self.wallet = MiniWallet(self.nodes[0])
+ self.nodes[0].get_wallet_rpc(self.default_wallet_name).sendtoaddress(self.wallet.get_address(), "5")
+ self.generate(self.wallet, 1, sync_fun=self.no_op)
+ self.nodes[0].createwallet("watch_wallet", disable_private_keys=True)
+ watch_wallet = self.nodes[0].get_wallet_rpc("watch_wallet")
+ watch_wallet.importaddress(self.wallet.get_address())
+
+ # DEFAULT_ANCESTOR_LIMIT transactions off a confirmed tx should be fine
+ chain = self.wallet.create_self_transfer_chain(chain_length=DEFAULT_ANCESTOR_LIMIT)
+ ancestor_vsize = 0
+ ancestor_fees = Decimal(0)
+
+ for i, t in enumerate(chain):
+ ancestor_vsize += t["tx"].get_vsize()
+ ancestor_fees += t["fee"]
+ self.wallet.sendrawtransaction(from_node=self.nodes[0], tx_hex=t["hex"])
+ # Check that listunspent ancestor{count, size, fees} yield the correct results
+ wallet_unspent = watch_wallet.listunspent(minconf=0)
+ this_unspent = next(utxo_info for utxo_info in wallet_unspent if utxo_info["txid"] == t["txid"])
+ assert_equal(this_unspent['ancestorcount'], i + 1)
+ assert_equal(this_unspent['ancestorsize'], ancestor_vsize)
+ assert_equal(this_unspent['ancestorfees'], ancestor_fees * COIN)
+
if __name__ == '__main__':
WalletTest().main()
diff --git a/test/functional/wallet_conflicts.py b/test/functional/wallet_conflicts.py
new file mode 100755
index 0000000000..802b718cd5
--- /dev/null
+++ b/test/functional/wallet_conflicts.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+# Copyright (c) 2023 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 that wallet correctly tracks transactions that have been conflicted by blocks, particularly during reorgs.
+"""
+
+from decimal import Decimal
+
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.util import (
+ assert_equal,
+)
+
+class TxConflicts(BitcoinTestFramework):
+ def add_options(self, parser):
+ self.add_wallet_options(parser)
+
+ def set_test_params(self):
+ self.num_nodes = 3
+
+ def skip_test_if_missing_module(self):
+ self.skip_if_no_wallet()
+
+ def get_utxo_of_value(self, from_tx_id, search_value):
+ return next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction(from_tx_id)["details"] if tx_out["amount"] == Decimal(f"{search_value}"))
+
+ def run_test(self):
+ self.log.info("Send tx from which to conflict outputs later")
+ txid_conflict_from_1 = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10"))
+ txid_conflict_from_2 = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10"))
+ self.generate(self.nodes[0], 1)
+ self.sync_blocks()
+
+ self.log.info("Disconnect nodes to broadcast conflicts on their respective chains")
+ self.disconnect_nodes(0, 1)
+ self.disconnect_nodes(2, 1)
+
+ self.log.info("Create transactions that conflict with each other")
+ output_A = self.get_utxo_of_value(from_tx_id=txid_conflict_from_1, search_value=10)
+ output_B = self.get_utxo_of_value(from_tx_id=txid_conflict_from_2, search_value=10)
+
+ # First create a transaction that consumes both A and B outputs.
+ #
+ # | tx1 | -----> | | | |
+ # | AB_parent_tx | ----> | Child_Tx |
+ # | tx2 | -----> | | | |
+ #
+ inputs_tx_AB_parent = [{"txid": txid_conflict_from_1, "vout": output_A}, {"txid": txid_conflict_from_2, "vout": output_B}]
+ tx_AB_parent = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs_tx_AB_parent, {self.nodes[0].getnewaddress(): Decimal("19.99998")}))
+
+ # Secondly, create two transactions: One consuming output_A, and another one consuming output_B
+ #
+ # | tx1 | -----> | Tx_A_1 |
+ # ----------------
+ # | tx2 | -----> | Tx_B_1 |
+ #
+ inputs_tx_A_1 = [{"txid": txid_conflict_from_1, "vout": output_A}]
+ inputs_tx_B_1 = [{"txid": txid_conflict_from_2, "vout": output_B}]
+ tx_A_1 = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs_tx_A_1, {self.nodes[0].getnewaddress(): Decimal("9.99998")}))
+ tx_B_1 = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs_tx_B_1, {self.nodes[0].getnewaddress(): Decimal("9.99998")}))
+
+ self.log.info("Broadcast conflicted transaction")
+ txid_AB_parent = self.nodes[0].sendrawtransaction(tx_AB_parent["hex"])
+ self.generate(self.nodes[0], 1, sync_fun=self.no_op)
+
+ # Now that 'AB_parent_tx' was broadcast, build 'Child_Tx'
+ output_c = self.get_utxo_of_value(from_tx_id=txid_AB_parent, search_value=19.99998)
+ inputs_tx_C_child = [({"txid": txid_AB_parent, "vout": output_c})]
+
+ tx_C_child = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs_tx_C_child, {self.nodes[0].getnewaddress() : Decimal("19.99996")}))
+ tx_C_child_txid = self.nodes[0].sendrawtransaction(tx_C_child["hex"])
+ self.generate(self.nodes[0], 1, sync_fun=self.no_op)
+
+ self.log.info("Broadcast conflicting tx to node 1 and generate a longer chain")
+ conflicting_txid_A = self.nodes[1].sendrawtransaction(tx_A_1["hex"])
+ self.generate(self.nodes[1], 4, sync_fun=self.no_op)
+ conflicting_txid_B = self.nodes[1].sendrawtransaction(tx_B_1["hex"])
+ self.generate(self.nodes[1], 4, sync_fun=self.no_op)
+
+ self.log.info("Connect nodes 0 and 1, trigger reorg and ensure that the tx is effectively conflicted")
+ self.connect_nodes(0, 1)
+ self.sync_blocks([self.nodes[0], self.nodes[1]])
+ conflicted_AB_tx = self.nodes[0].gettransaction(txid_AB_parent)
+ tx_C_child = self.nodes[0].gettransaction(tx_C_child_txid)
+ conflicted_A_tx = self.nodes[0].gettransaction(conflicting_txid_A)
+
+ self.log.info("Verify, after the reorg, that Tx_A was accepted, and tx_AB and its Child_Tx are conflicting now")
+ # Tx A was accepted, Tx AB was not.
+ assert conflicted_AB_tx["confirmations"] < 0
+ assert conflicted_A_tx["confirmations"] > 0
+
+ # Conflicted tx should have confirmations set to the confirmations of the most conflicting tx
+ assert_equal(-conflicted_AB_tx["confirmations"], conflicted_A_tx["confirmations"])
+ # Child should inherit conflicted state from parent
+ assert_equal(-tx_C_child["confirmations"], conflicted_A_tx["confirmations"])
+ # Check the confirmations of the conflicting transactions
+ assert_equal(conflicted_A_tx["confirmations"], 8)
+ assert_equal(self.nodes[0].gettransaction(conflicting_txid_B)["confirmations"], 4)
+
+ self.log.info("Now generate a longer chain that does not contain any tx")
+ # Node2 chain without conflicts
+ self.generate(self.nodes[2], 15, sync_fun=self.no_op)
+
+ # Connect node0 and node2 and wait reorg
+ self.connect_nodes(0, 2)
+ self.sync_blocks()
+ conflicted = self.nodes[0].gettransaction(txid_AB_parent)
+ tx_C_child = self.nodes[0].gettransaction(tx_C_child_txid)
+
+ self.log.info("Test that formerly conflicted transaction are inactive after reorg")
+ # Former conflicted tx should be unconfirmed as it hasn't been yet rebroadcast
+ assert_equal(conflicted["confirmations"], 0)
+ # Former conflicted child tx should be unconfirmed as it hasn't been rebroadcast
+ assert_equal(tx_C_child["confirmations"], 0)
+ # Rebroadcast former conflicted tx and check it confirms smoothly
+ self.nodes[2].sendrawtransaction(conflicted["hex"])
+ self.generate(self.nodes[2], 1)
+ self.sync_blocks()
+ former_conflicted = self.nodes[0].gettransaction(txid_AB_parent)
+ assert_equal(former_conflicted["confirmations"], 1)
+ assert_equal(former_conflicted["blockheight"], 217)
+
+if __name__ == '__main__':
+ TxConflicts().main()