aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rwxr-xr-xtest/functional/feature_config_args.py58
-rwxr-xr-xtest/functional/feature_proxy.py4
-rwxr-xr-xtest/functional/feature_utxo_set_hash.py86
-rwxr-xr-xtest/functional/interface_zmq.py76
-rwxr-xr-xtest/functional/rpc_blockchain.py12
-rwxr-xr-xtest/functional/rpc_net.py6
-rwxr-xr-xtest/functional/rpc_uptime.py5
-rw-r--r--test/functional/test_framework/key.py6
-rwxr-xr-xtest/functional/test_framework/messages.py6
-rwxr-xr-xtest/functional/test_framework/test_node.py2
-rwxr-xr-xtest/functional/test_runner.py1
-rwxr-xr-xtest/lint/lint-circular-dependencies.sh1
-rwxr-xr-xtest/lint/lint-includes.sh2
-rwxr-xr-xtest/lint/lint-python-dead-code.sh23
-rw-r--r--test/sanitizer_suppressions/tsan1
15 files changed, 254 insertions, 35 deletions
diff --git a/test/functional/feature_config_args.py b/test/functional/feature_config_args.py
index 2445b6d977..573760a8cb 100755
--- a/test/functional/feature_config_args.py
+++ b/test/functional/feature_config_args.py
@@ -5,6 +5,7 @@
"""Test various command line arguments and configuration file parameters."""
import os
+import time
from test_framework.test_framework import BitcoinTestFramework
from test_framework import util
@@ -147,11 +148,68 @@ class ConfArgsTest(BitcoinTestFramework):
self.start_node(0, extra_args=['-nonetworkactive=1'])
self.stop_node(0)
+ def test_seed_peers(self):
+ self.log.info('Test seed peers')
+ default_data_dir = self.nodes[0].datadir
+
+ # No peers.dat exists and -dnsseed=1
+ # We expect the node will use DNS Seeds, but Regtest mode has 0 DNS seeds
+ # So after 60 seconds, the node should fallback to fixed seeds (this is a slow test)
+ assert not os.path.exists(os.path.join(default_data_dir, "peers.dat"))
+ start = int(time.time())
+ with self.nodes[0].assert_debug_log(expected_msgs=[
+ "Loaded 0 addresses from peers.dat",
+ "0 addresses found from DNS seeds"]):
+ self.start_node(0, extra_args=['-dnsseed=1 -mocktime={}'.format(start)])
+ with self.nodes[0].assert_debug_log(expected_msgs=[
+ "Adding fixed seeds as 60 seconds have passed and addrman is empty"]):
+ self.nodes[0].setmocktime(start + 65)
+ self.stop_node(0)
+
+ # No peers.dat exists and -dnsseed=0
+ # We expect the node will fallback immediately to fixed seeds
+ assert not os.path.exists(os.path.join(default_data_dir, "peers.dat"))
+ start = time.time()
+ with self.nodes[0].assert_debug_log(expected_msgs=[
+ "Loaded 0 addresses from peers.dat",
+ "DNS seeding disabled",
+ "Adding fixed seeds as -dnsseed=0, -addnode is not provided and all -seednode(s) attempted\n"]):
+ self.start_node(0, extra_args=['-dnsseed=0'])
+ assert time.time() - start < 60
+ self.stop_node(0)
+
+ # No peers.dat exists and dns seeds are disabled.
+ # We expect the node will not add fixed seeds when explicitly disabled.
+ assert not os.path.exists(os.path.join(default_data_dir, "peers.dat"))
+ start = time.time()
+ with self.nodes[0].assert_debug_log(expected_msgs=[
+ "Loaded 0 addresses from peers.dat",
+ "DNS seeding disabled",
+ "Fixed seeds are disabled"]):
+ self.start_node(0, extra_args=['-dnsseed=0', '-fixedseeds=0'])
+ assert time.time() - start < 60
+ self.stop_node(0)
+
+ # No peers.dat exists and -dnsseed=0, but a -addnode is provided
+ # We expect the node will allow 60 seconds prior to using fixed seeds
+ assert not os.path.exists(os.path.join(default_data_dir, "peers.dat"))
+ start = int(time.time())
+ with self.nodes[0].assert_debug_log(expected_msgs=[
+ "Loaded 0 addresses from peers.dat",
+ "DNS seeding disabled"]):
+ self.start_node(0, extra_args=['-dnsseed=0', '-addnode=fakenodeaddr -mocktime={}'.format(start)])
+ with self.nodes[0].assert_debug_log(expected_msgs=[
+ "Adding fixed seeds as 60 seconds have passed and addrman is empty"]):
+ self.nodes[0].setmocktime(start + 65)
+ self.stop_node(0)
+
+
def run_test(self):
self.stop_node(0)
self.test_log_buffer()
self.test_args_log()
+ self.test_seed_peers()
self.test_networkactive()
self.test_config_file_parser()
diff --git a/test/functional/feature_proxy.py b/test/functional/feature_proxy.py
index cd5eff9184..2983feaa0d 100755
--- a/test/functional/feature_proxy.py
+++ b/test/functional/feature_proxy.py
@@ -44,8 +44,8 @@ from test_framework.netutil import test_ipv6_local
RANGE_BEGIN = PORT_MIN + 2 * PORT_RANGE # Start after p2p and rpc ports
-# Networks returned by RPC getpeerinfo, defined in src/netbase.cpp::GetNetworkName()
-NET_UNROUTABLE = "unroutable"
+# Networks returned by RPC getpeerinfo.
+NET_UNROUTABLE = "not_publicly_routable"
NET_IPV4 = "ipv4"
NET_IPV6 = "ipv6"
NET_ONION = "onion"
diff --git a/test/functional/feature_utxo_set_hash.py b/test/functional/feature_utxo_set_hash.py
new file mode 100755
index 0000000000..6e6046d84d
--- /dev/null
+++ b/test/functional/feature_utxo_set_hash.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+# Copyright (c) 2020-2021 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 UTXO set hash value calculation in gettxoutsetinfo."""
+
+import struct
+
+from test_framework.blocktools import create_transaction
+from test_framework.messages import (
+ CBlock,
+ COutPoint,
+ FromHex,
+)
+from test_framework.muhash import MuHash3072
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.util import assert_equal
+
+class UTXOSetHashTest(BitcoinTestFramework):
+ def set_test_params(self):
+ self.num_nodes = 1
+ self.setup_clean_chain = True
+
+ def skip_test_if_missing_module(self):
+ self.skip_if_no_wallet()
+
+ def test_deterministic_hash_results(self):
+ self.log.info("Test deterministic UTXO set hash results")
+
+ # These depend on the setup_clean_chain option, the chain loaded from the cache
+ assert_equal(self.nodes[0].gettxoutsetinfo()['hash_serialized_2'], "b32ec1dda5a53cd025b95387aad344a801825fe46a60ff952ce26528f01d3be8")
+ assert_equal(self.nodes[0].gettxoutsetinfo("muhash")['muhash'], "dd5ad2a105c2d29495f577245c357409002329b9f4d6182c0af3dc2f462555c8")
+
+ def test_muhash_implementation(self):
+ self.log.info("Test MuHash implementation consistency")
+
+ node = self.nodes[0]
+
+ # Generate 100 blocks and remove the first since we plan to spend its
+ # coinbase
+ block_hashes = node.generate(100)
+ blocks = list(map(lambda block: FromHex(CBlock(), node.getblock(block, False)), block_hashes))
+ spending = blocks.pop(0)
+
+ # Create a spending transaction and mine a block which includes it
+ tx = create_transaction(node, spending.vtx[0].rehash(), node.getnewaddress(), amount=49)
+ txid = node.sendrawtransaction(hexstring=tx.serialize_with_witness().hex(), maxfeerate=0)
+
+ tx_block = node.generateblock(output=node.getnewaddress(), transactions=[txid])
+ blocks.append(FromHex(CBlock(), node.getblock(tx_block['hash'], False)))
+
+ # Serialize the outputs that should be in the UTXO set and add them to
+ # a MuHash object
+ muhash = MuHash3072()
+
+ for height, block in enumerate(blocks):
+ # The Genesis block coinbase is not part of the UTXO set and we
+ # spent the first mined block
+ height += 2
+
+ for tx in block.vtx:
+ for n, tx_out in enumerate(tx.vout):
+ coinbase = 1 if not tx.vin[0].prevout.hash else 0
+
+ # Skip witness commitment
+ if (coinbase and n > 0):
+ continue
+
+ data = COutPoint(int(tx.rehash(), 16), n).serialize()
+ data += struct.pack("<i", height * 2 + coinbase)
+ data += tx_out.serialize()
+
+ muhash.insert(data)
+
+ finalized = muhash.digest()
+ node_muhash = node.gettxoutsetinfo("muhash")['muhash']
+
+ assert_equal(finalized[::-1].hex(), node_muhash)
+
+ def run_test(self):
+ self.test_deterministic_hash_results()
+ self.test_muhash_implementation()
+
+
+if __name__ == '__main__':
+ UTXOSetHashTest().main()
diff --git a/test/functional/interface_zmq.py b/test/functional/interface_zmq.py
index e9f61be4d4..d0967a9340 100755
--- a/test/functional/interface_zmq.py
+++ b/test/functional/interface_zmq.py
@@ -27,28 +27,31 @@ def hash256_reversed(byte_str):
class ZMQSubscriber:
def __init__(self, socket, topic):
- self.sequence = 0
+ self.sequence = None # no sequence number received yet
self.socket = socket
self.topic = topic
self.socket.setsockopt(zmq.SUBSCRIBE, self.topic)
- def receive(self):
+ # Receive message from publisher and verify that topic and sequence match
+ def _receive_from_publisher_and_check(self):
topic, body, seq = self.socket.recv_multipart()
# Topic should match the subscriber topic.
assert_equal(topic, self.topic)
# Sequence should be incremental.
- assert_equal(struct.unpack('<I', seq)[-1], self.sequence)
+ received_seq = struct.unpack('<I', seq)[-1]
+ if self.sequence is None:
+ self.sequence = received_seq
+ else:
+ assert_equal(received_seq, self.sequence)
self.sequence += 1
return body
+ def receive(self):
+ return self._receive_from_publisher_and_check()
+
def receive_sequence(self):
- topic, body, seq = self.socket.recv_multipart()
- # Topic should match the subscriber topic.
- assert_equal(topic, self.topic)
- # Sequence should be incremental.
- assert_equal(struct.unpack('<I', seq)[-1], self.sequence)
- self.sequence += 1
+ body = self._receive_from_publisher_and_check()
hash = body[:32].hex()
label = chr(body[32])
mempool_sequence = None if len(body) != 32+1+8 else struct.unpack("<Q", body[32+1:])[0]
@@ -64,6 +67,9 @@ class ZMQTest (BitcoinTestFramework):
self.num_nodes = 2
if self.is_wallet_compiled():
self.requires_wallet = True
+ # This test isn't testing txn relay/timing, so set whitelist on the
+ # peers for instant txn relay. This speeds up the test run time 2-3x.
+ self.extra_args = [["-whitelist=noban@127.0.0.1"]] * self.num_nodes
def skip_test_if_missing_module(self):
self.skip_if_no_py3_zmq()
@@ -84,23 +90,46 @@ class ZMQTest (BitcoinTestFramework):
# Restart node with the specified zmq notifications enabled, subscribe to
# all of them and return the corresponding ZMQSubscriber objects.
- def setup_zmq_test(self, services, recv_timeout=60, connect_nodes=False):
+ def setup_zmq_test(self, services, *, recv_timeout=60, sync_blocks=True):
subscribers = []
for topic, address in services:
socket = self.ctx.socket(zmq.SUB)
- socket.set(zmq.RCVTIMEO, recv_timeout*1000)
subscribers.append(ZMQSubscriber(socket, topic.encode()))
- self.restart_node(0, ["-zmqpub%s=%s" % (topic, address) for topic, address in services])
-
- if connect_nodes:
- self.connect_nodes(0, 1)
+ self.restart_node(0, ["-zmqpub%s=%s" % (topic, address) for topic, address in services] +
+ self.extra_args[0])
for i, sub in enumerate(subscribers):
sub.socket.connect(services[i][1])
- # Relax so that the subscribers are ready before publishing zmq messages
- sleep(0.2)
+ # Ensure that all zmq publisher notification interfaces are ready by
+ # running the following "sync up" procedure:
+ # 1. Generate a block on the node
+ # 2. Try to receive a notification on all subscribers
+ # 3. If all subscribers get a message within the timeout (1 second),
+ # we are done, otherwise repeat starting from step 1
+ for sub in subscribers:
+ sub.socket.set(zmq.RCVTIMEO, 1000)
+ while True:
+ self.nodes[0].generate(1)
+ recv_failed = False
+ for sub in subscribers:
+ try:
+ sub.receive()
+ except zmq.error.Again:
+ self.log.debug("Didn't receive sync-up notification, trying again.")
+ recv_failed = True
+ if not recv_failed:
+ self.log.debug("ZMQ sync-up completed, all subscribers are ready.")
+ break
+
+ # set subscriber's desired timeout for the test
+ for sub in subscribers:
+ sub.socket.set(zmq.RCVTIMEO, recv_timeout*1000)
+
+ self.connect_nodes(0, 1)
+ if sync_blocks:
+ self.sync_blocks()
return subscribers
@@ -110,9 +139,7 @@ class ZMQTest (BitcoinTestFramework):
self.restart_node(0, ["-zmqpubrawtx=foo", "-zmqpubhashtx=bar"])
address = 'tcp://127.0.0.1:28332'
- subs = self.setup_zmq_test(
- [(topic, address) for topic in ["hashblock", "hashtx", "rawblock", "rawtx"]],
- connect_nodes=True)
+ subs = self.setup_zmq_test([(topic, address) for topic in ["hashblock", "hashtx", "rawblock", "rawtx"]])
hashblock = subs[0]
hashtx = subs[1]
@@ -189,6 +216,7 @@ class ZMQTest (BitcoinTestFramework):
hashblock, hashtx = self.setup_zmq_test(
[(topic, address) for topic in ["hashblock", "hashtx"]],
recv_timeout=2) # 2 second timeout to check end of notifications
+ self.disconnect_nodes(0, 1)
# Generate 1 block in nodes[0] with 1 mempool tx and receive all notifications
payment_txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1.0)
@@ -237,6 +265,7 @@ class ZMQTest (BitcoinTestFramework):
"""
self.log.info("Testing 'sequence' publisher")
[seq] = self.setup_zmq_test([("sequence", "tcp://127.0.0.1:28333")])
+ self.disconnect_nodes(0, 1)
# Mempool sequence number starts at 1
seq_num = 1
@@ -387,7 +416,7 @@ class ZMQTest (BitcoinTestFramework):
return
self.log.info("Testing 'mempool sync' usage of sequence notifier")
- [seq] = self.setup_zmq_test([("sequence", "tcp://127.0.0.1:28333")], connect_nodes=True)
+ [seq] = self.setup_zmq_test([("sequence", "tcp://127.0.0.1:28333")])
# In-memory counter, should always start at 1
next_mempool_seq = self.nodes[0].getrawmempool(mempool_sequence=True)["mempool_sequence"]
@@ -487,10 +516,13 @@ class ZMQTest (BitcoinTestFramework):
def test_multiple_interfaces(self):
# Set up two subscribers with different addresses
+ # (note that after the reorg test, syncing would fail due to different
+ # chain lengths on node0 and node1; for this test we only need node0, so
+ # we can disable syncing blocks on the setup)
subscribers = self.setup_zmq_test([
("hashblock", "tcp://127.0.0.1:28334"),
("hashblock", "tcp://127.0.0.1:28335"),
- ])
+ ], sync_blocks=False)
# Generate 1 block in nodes[0] and receive all notifications
self.nodes[0].generatetoaddress(1, ADDRESS_BCRT1_UNSPENDABLE)
diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py
index 99be6b7b8e..84ca1b99c2 100755
--- a/test/functional/rpc_blockchain.py
+++ b/test/functional/rpc_blockchain.py
@@ -268,6 +268,18 @@ class BlockchainTest(BitcoinTestFramework):
res5 = node.gettxoutsetinfo(hash_type='none')
assert 'hash_serialized_2' not in res5
+ # hash_type muhash should return a different UTXO set hash.
+ res6 = node.gettxoutsetinfo(hash_type='muhash')
+ assert 'muhash' in res6
+ assert(res['hash_serialized_2'] != res6['muhash'])
+
+ # muhash should not be included in gettxoutset unless requested.
+ for r in [res, res2, res3, res4, res5]:
+ assert 'muhash' not in r
+
+ # Unknown hash_type raises an error
+ assert_raises_rpc_error(-8, "foohash is not a valid hash_type", node.gettxoutsetinfo, "foohash")
+
def _test_getblockheader(self):
node = self.nodes[0]
diff --git a/test/functional/rpc_net.py b/test/functional/rpc_net.py
index cf46616681..2d41963beb 100755
--- a/test/functional/rpc_net.py
+++ b/test/functional/rpc_net.py
@@ -104,6 +104,9 @@ class NetTest(BitcoinTestFramework):
assert_equal(peer_info[1][0]['connection_type'], 'manual')
assert_equal(peer_info[1][1]['connection_type'], 'inbound')
+ # Check dynamically generated networks list in getpeerinfo help output.
+ assert "(ipv4, ipv6, onion, not_publicly_routable)" in self.nodes[0].help("getpeerinfo")
+
def test_getnettotals(self):
self.log.info("Test getnettotals")
# Test getnettotals and getpeerinfo by doing a ping. The bytes
@@ -152,6 +155,9 @@ class NetTest(BitcoinTestFramework):
for info in network_info:
assert_net_servicesnames(int(info["localservices"], 0x10), info["localservicesnames"])
+ # Check dynamically generated networks list in getnetworkinfo help output.
+ assert "(ipv4, ipv6, onion)" in self.nodes[0].help("getnetworkinfo")
+
def test_getaddednodeinfo(self):
self.log.info("Test getaddednodeinfo")
assert_equal(self.nodes[0].getaddednodeinfo(), [])
diff --git a/test/functional/rpc_uptime.py b/test/functional/rpc_uptime.py
index e86f91b1d0..6177970872 100755
--- a/test/functional/rpc_uptime.py
+++ b/test/functional/rpc_uptime.py
@@ -10,6 +10,7 @@ Test corresponds to code in rpc/server.cpp.
import time
from test_framework.test_framework import BitcoinTestFramework
+from test_framework.util import assert_raises_rpc_error
class UptimeTest(BitcoinTestFramework):
@@ -18,8 +19,12 @@ class UptimeTest(BitcoinTestFramework):
self.setup_clean_chain = True
def run_test(self):
+ self._test_negative_time()
self._test_uptime()
+ def _test_negative_time(self):
+ assert_raises_rpc_error(-8, "Mocktime can not be negative: -1.", self.nodes[0].setmocktime, -1)
+
def _test_uptime(self):
wait_time = 10
self.nodes[0].setmocktime(int(time.time() + wait_time))
diff --git a/test/functional/test_framework/key.py b/test/functional/test_framework/key.py
index e0cbab45ce..26526e35fa 100644
--- a/test/functional/test_framework/key.py
+++ b/test/functional/test_framework/key.py
@@ -20,10 +20,6 @@ def TaggedHash(tag, data):
ss += data
return hashlib.sha256(ss).digest()
-def xor_bytes(b0, b1):
- assert len(b0) == len(b1)
- return bytes(x ^ y for (x, y) in zip(b0, b1))
-
def jacobi_symbol(n, k):
"""Compute the Jacobi symbol of n modulo k
@@ -510,7 +506,7 @@ class TestFrameworkKey(unittest.TestCase):
if pubkey is not None:
keys[privkey] = pubkey
for msg in byte_arrays: # test every combination of message, signing key, verification key
- for sign_privkey, sign_pubkey in keys.items():
+ for sign_privkey, _ in keys.items():
sig = sign_schnorr(sign_privkey, msg)
for verify_privkey, verify_pubkey in keys.items():
if verify_privkey == sign_privkey:
diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py
index 27a09ef86c..561d1813c1 100755
--- a/test/functional/test_framework/messages.py
+++ b/test/functional/test_framework/messages.py
@@ -33,7 +33,7 @@ from test_framework.util import hex_str_to_bytes, assert_equal
MIN_VERSION_SUPPORTED = 60001
MY_VERSION = 70016 # past wtxid relay
-MY_SUBVERSION = b"/python-p2p-tester:0.0.3/"
+MY_SUBVERSION = "/python-p2p-tester:0.0.3/"
MY_RELAY = 1 # from version 70001 onwards, fRelay should be appended to version messages (BIP37)
MAX_LOCATOR_SZ = 101
@@ -1048,7 +1048,7 @@ class msg_version:
self.addrFrom = CAddress()
self.addrFrom.deserialize(f, with_time=False)
self.nNonce = struct.unpack("<Q", f.read(8))[0]
- self.strSubVer = deser_string(f)
+ self.strSubVer = deser_string(f).decode('utf-8')
self.nStartingHeight = struct.unpack("<i", f.read(4))[0]
@@ -1069,7 +1069,7 @@ class msg_version:
r += self.addrTo.serialize(with_time=False)
r += self.addrFrom.serialize(with_time=False)
r += struct.pack("<Q", self.nNonce)
- r += ser_string(self.strSubVer)
+ r += ser_string(self.strSubVer.encode('utf-8'))
r += struct.pack("<i", self.nStartingHeight)
r += struct.pack("<b", self.nRelay)
return r
diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py
index b61d433652..9f2b570913 100755
--- a/test/functional/test_framework/test_node.py
+++ b/test/functional/test_framework/test_node.py
@@ -572,7 +572,7 @@ class TestNode():
def num_test_p2p_connections(self):
"""Return number of test framework p2p connections to the node."""
- return len([peer for peer in self.getpeerinfo() if peer['subver'] == MY_SUBVERSION.decode("utf-8")])
+ return len([peer for peer in self.getpeerinfo() if peer['subver'] == MY_SUBVERSION])
def disconnect_p2ps(self):
"""Close all p2p connections to the node."""
diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py
index d744ac218c..d742ef4eee 100755
--- a/test/functional/test_runner.py
+++ b/test/functional/test_runner.py
@@ -203,6 +203,7 @@ BASE_SCRIPTS = [
'feature_notifications.py',
'rpc_getblockfilter.py',
'rpc_invalidateblock.py',
+ 'feature_utxo_set_hash.py',
'feature_rbf.py',
'mempool_packages.py',
'mempool_package_onemore.py',
diff --git a/test/lint/lint-circular-dependencies.sh b/test/lint/lint-circular-dependencies.sh
index 509c9231d2..0b15f99448 100755
--- a/test/lint/lint-circular-dependencies.sh
+++ b/test/lint/lint-circular-dependencies.sh
@@ -21,6 +21,7 @@ EXPECTED_CIRCULAR_DEPENDENCIES=(
"txmempool -> validation -> txmempool"
"wallet/fees -> wallet/wallet -> wallet/fees"
"wallet/wallet -> wallet/walletdb -> wallet/wallet"
+ "node/coinstats -> validation -> node/coinstats"
)
EXIT_CODE=0
diff --git a/test/lint/lint-includes.sh b/test/lint/lint-includes.sh
index fc0b86a297..bf7aeb5b4f 100755
--- a/test/lint/lint-includes.sh
+++ b/test/lint/lint-includes.sh
@@ -65,8 +65,6 @@ EXPECTED_BOOST_INCLUDES=(
boost/signals2/optional_last_value.hpp
boost/signals2/signal.hpp
boost/test/unit_test.hpp
- boost/thread/lock_types.hpp
- boost/thread/shared_mutex.hpp
)
for BOOST_INCLUDE in $(git grep '^#include <boost/' -- "*.cpp" "*.h" | cut -f2 -d: | cut -f2 -d'<' | cut -f1 -d'>' | sort -u); do
diff --git a/test/lint/lint-python-dead-code.sh b/test/lint/lint-python-dead-code.sh
new file mode 100755
index 0000000000..c3b6ff3c98
--- /dev/null
+++ b/test/lint/lint-python-dead-code.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+#
+# Copyright (c) 2021 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+#
+# Find dead Python code.
+
+export LC_ALL=C
+
+if ! command -v vulture > /dev/null; then
+ echo "Skipping Python dead code linting since vulture is not installed. Install by running \"pip3 install vulture\""
+ exit 0
+fi
+
+# --min-confidence 100 will only report code that is guaranteed to be unused within the analyzed files.
+# Any value below 100 introduces the risk of false positives, which would create an unacceptable maintenance burden.
+if ! vulture \
+ --min-confidence 100 \
+ $(git ls-files -- "*.py"); then
+ echo "Python dead code detection found some issues"
+ exit 1
+fi
diff --git a/test/sanitizer_suppressions/tsan b/test/sanitizer_suppressions/tsan
index 3a04418e8b..3fc9fac25c 100644
--- a/test/sanitizer_suppressions/tsan
+++ b/test/sanitizer_suppressions/tsan
@@ -28,6 +28,7 @@ race:BerkeleyBatch
race:BerkeleyDatabase
race:DatabaseBatch
race:leveldb::DBImpl::DeleteObsoleteFiles
+race:validation_chainstatemanager_tests
race:zmq::*
race:bitcoin-qt