Compare commits

...

6 Commits

Author SHA1 Message Date
hodlinator
578c4430e3
Merge a1da3bfc12f77aacae9fb409df40d5151465dea4 into 5f4422d68dc3530c353af1f87499de1c864b60ad 2025-03-17 09:50:15 +07:00
merge-script
5f4422d68d
Merge bitcoin/bitcoin#32010: qa: Fix TxIndex race conditions
3301d2cbe8c3b76c97285d75fa59637cb6952d0b qa: Wait for txindex to avoid race condition (Hodlinator)
9bfb0d75ba10591cc6c9620f9fd1ecc0e55e7a48 qa: Remove unnecessary -txindex args (Hodlinator)
7ac281c19cd3d11f316dbbb3308eabf1ad4f26d6 qa: Add missing coverage of corrupt indexes (Hodlinator)

Pull request description:

  - Add synchronization in 3 places where if the Transaction Index happens to be slow, we get rare test failures when querying it for transactions (one such case experienced on Windows, prompting investigation).
  - Remove unnecessary TxIndex initialization in some tests.
  - Add some test coverage where TxIndex aspect could be tested in feature_init.py.

ACKs for top commit:
  fjahr:
    re-ACK 3301d2cbe8c3b76c97285d75fa59637cb6952d0b
  mzumsande:
    Code Review ACK 3301d2cbe8c3b76c97285d75fa59637cb6952d0b
  furszy:
    Code review ACK 3301d2cbe8c3b76c97285d75fa59637cb6952d0b
  Prabhat1308:
    Concept ACK [`3301d2c`](3301d2cbe8)

Tree-SHA512: 7c2019e38455f344856aaf6b381faafbd88d53dc88d13309deb718c1dcfbee4ccca7c7f1b66917395503a6f94c3b216a007ad432cc8b93d0309db9805f38d602
2025-03-17 10:28:14 +08:00
Hodlinator
3301d2cbe8
qa: Wait for txindex to avoid race condition
Can be verified to be necessary through adding std::this_thread::sleep_for(0.5s) at the beginning of TxIndex::CustomAppend.
2025-03-10 15:24:16 +01:00
Hodlinator
9bfb0d75ba
qa: Remove unnecessary -txindex args
(Parent commit ensured indexes in feature_init.py are actually used, otherwise they would be removed here as well).
2025-03-07 22:22:31 +01:00
Hodlinator
7ac281c19c
qa: Add missing coverage of corrupt indexes 2025-03-07 22:22:31 +01:00
Hodlinator
a1da3bfc12
qa debug: Add --debug_runs/-waitfordebugger
Makes bitcoind spin during startup, waiting for a debugger to be attached.

Useful for debugging one or several bitcoind nodes running in the context of a functional test.

TODO: Confirm code works on Mac.
2025-02-12 22:58:00 +01:00
11 changed files with 129 additions and 10 deletions

View File

@ -132,6 +132,7 @@ option(ENABLE_HARDENING "Attempt to harden the resulting executables." ON)
option(REDUCE_EXPORTS "Attempt to reduce exported symbols in the resulting executables." OFF) option(REDUCE_EXPORTS "Attempt to reduce exported symbols in the resulting executables." OFF)
option(WERROR "Treat compiler warnings as errors." OFF) option(WERROR "Treat compiler warnings as errors." OFF)
option(WITH_CCACHE "Attempt to use ccache for compiling." ON) option(WITH_CCACHE "Attempt to use ccache for compiling." ON)
option(WAIT_FOR_DEBUGGER "Support for waiting during startup for debugger to be attached." OFF)
option(WITH_ZMQ "Enable ZMQ notifications." OFF) option(WITH_ZMQ "Enable ZMQ notifications." OFF)
if(WITH_ZMQ) if(WITH_ZMQ)
@ -246,6 +247,10 @@ target_compile_definitions(core_interface_debug INTERFACE
ABORT_ON_FAILED_ASSUME ABORT_ON_FAILED_ASSUME
) )
if(WAIT_FOR_DEBUGGER)
add_compile_definitions(WAIT_FOR_DEBUGGER=1)
endif()
if(WIN32) if(WIN32)
#[=[ #[=[
This build system supports two ways to build binaries for Windows. This build system supports two ways to build binaries for Windows.

View File

@ -28,7 +28,19 @@
#include <util/tokenpipe.h> #include <util/tokenpipe.h>
#include <util/translation.h> #include <util/translation.h>
#ifdef WIN32
#include <windows.h>
#include <debugapi.h>
#elif defined(__APPLE__)
#include <sys/sysctl.h>
#elif defined(__linux__)
#include <linux/prctl.h>
#include <sys/prctl.h>
#endif
#include <any> #include <any>
#include <cstdlib>
#include <fstream>
#include <functional> #include <functional>
#include <optional> #include <optional>
@ -159,6 +171,60 @@ static bool ProcessInitCommands(ArgsManager& args)
return false; return false;
} }
#ifdef WAIT_FOR_DEBUGGER
static int HandleWaitForDebugger(int argc, char* argv[])
{
for (int i = 0; i < argc; ++i) {
if (strcmp(argv[i], "-waitfordebugger") == 0) {
while (true) {
bool attached{false};
# if defined(__linux__)
// Allow any process to attach to us.
prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY);
std::ifstream sf{"/proc/self/status", std::ios::in};
if (!sf.good()) {
return EXIT_FAILURE;
}
std::string s;
uint pid{0};
while (sf >> s) {
if (s == "TracerPid:") {
sf >> pid;
break;
}
std::getline(sf, s);
}
attached = pid > 0;
# elif defined(__APPLE__)
const int mib[] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid() };
kinfo_proc info;
size_t size{sizeof(info)};
const int ret{sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, nullptr, 0)};
if (ret != EXIT_SUCCESS) {
return ret;
}
attached = info.kp_proc.p_flag & P_TRACED;
# elif defined(WIN32)
attached = IsDebuggerPresent();
# else
# error "Platform doesn't support -waitfordebugger.";
# endif // platform
if (attached) {
break;
} else {
std::this_thread::sleep_for(100ms);
}
}
}
}
return EXIT_SUCCESS;
}
#endif // WAIT_FOR_DEBUGGER
static bool AppInit(NodeContext& node) static bool AppInit(NodeContext& node)
{ {
bool fRet = false; bool fRet = false;
@ -254,6 +320,10 @@ static bool AppInit(NodeContext& node)
MAIN_FUNCTION MAIN_FUNCTION
{ {
#ifdef WAIT_FOR_DEBUGGER
if (int ret{HandleWaitForDebugger(argc, argv)}; ret != EXIT_SUCCESS) return ret;
#endif
#ifdef WIN32 #ifdef WIN32
common::WinCmdLineArgs winArgs; common::WinCmdLineArgs winArgs;
std::tie(argc, argv) = winArgs.get(); std::tie(argc, argv) = winArgs.get();

View File

@ -681,6 +681,7 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc)
hidden_args.emplace_back("-daemon"); hidden_args.emplace_back("-daemon");
hidden_args.emplace_back("-daemonwait"); hidden_args.emplace_back("-daemonwait");
#endif #endif
argsman.AddArg("-waitfordebugger", "Spin until a debugger is attached", ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST);
// Add the hidden options // Add the hidden options
argsman.AddHiddenArgs(hidden_args); argsman.AddHiddenArgs(hidden_args);
@ -1119,6 +1120,12 @@ bool AppInitParameterInteraction(const ArgsManager& args)
} }
} }
#ifndef WAIT_FOR_DEBUGGER
if (args.GetBoolArg("-waitfordebugger", false)) {
return InitError(Untranslated("-waitfordebugger support was not included as WAIT_FOR_DEBUGGER is not #defined."));
}
#endif
return true; return true;
} }

View File

@ -88,7 +88,7 @@ class InitTest(BitcoinTestFramework):
args = ['-txindex=1', '-blockfilterindex=1', '-coinstatsindex=1'] args = ['-txindex=1', '-blockfilterindex=1', '-coinstatsindex=1']
for terminate_line in lines_to_terminate_after: for terminate_line in lines_to_terminate_after:
self.log.info(f"Starting node and will exit after line {terminate_line}") self.log.info(f"Starting node and will terminate after line {terminate_line}")
with node.busy_wait_for_debug_log([terminate_line]): with node.busy_wait_for_debug_log([terminate_line]):
if platform.system() == 'Windows': if platform.system() == 'Windows':
# CREATE_NEW_PROCESS_GROUP is required in order to be able # CREATE_NEW_PROCESS_GROUP is required in order to be able
@ -108,12 +108,22 @@ class InitTest(BitcoinTestFramework):
'blocks/index/*.ldb': 'Error opening block database.', 'blocks/index/*.ldb': 'Error opening block database.',
'chainstate/*.ldb': 'Error opening coins database.', 'chainstate/*.ldb': 'Error opening coins database.',
'blocks/blk*.dat': 'Error loading block database.', 'blocks/blk*.dat': 'Error loading block database.',
'indexes/txindex/MANIFEST*': 'LevelDB error: Corruption: CURRENT points to a non-existent file',
# Removing these files does not result in a startup error:
# 'indexes/blockfilter/basic/*.dat', 'indexes/blockfilter/basic/db/*.*', 'indexes/coinstats/db/*.*',
# 'indexes/txindex/*.log', 'indexes/txindex/CURRENT', 'indexes/txindex/LOCK'
} }
files_to_perturb = { files_to_perturb = {
'blocks/index/*.ldb': 'Error loading block database.', 'blocks/index/*.ldb': 'Error loading block database.',
'chainstate/*.ldb': 'Error opening coins database.', 'chainstate/*.ldb': 'Error opening coins database.',
'blocks/blk*.dat': 'Corrupted block database detected.', 'blocks/blk*.dat': 'Corrupted block database detected.',
'indexes/blockfilter/basic/db/*.*': 'LevelDB error: Corruption',
'indexes/coinstats/db/*.*': 'LevelDB error: Corruption',
'indexes/txindex/*.log': 'LevelDB error: Corruption',
'indexes/txindex/CURRENT': 'LevelDB error: Corruption',
# Perturbing these files does not result in a startup error:
# 'indexes/blockfilter/basic/*.dat', 'indexes/txindex/MANIFEST*', 'indexes/txindex/LOCK'
} }
for file_patt, err_fragment in files_to_delete.items(): for file_patt, err_fragment in files_to_delete.items():
@ -135,9 +145,10 @@ class InitTest(BitcoinTestFramework):
self.stop_node(0) self.stop_node(0)
self.log.info("Test startup errors after perturbing certain essential files") self.log.info("Test startup errors after perturbing certain essential files")
dirs = ["blocks", "chainstate", "indexes"]
for file_patt, err_fragment in files_to_perturb.items(): for file_patt, err_fragment in files_to_perturb.items():
shutil.copytree(node.chain_path / "blocks", node.chain_path / "blocks_bak") for dir in dirs:
shutil.copytree(node.chain_path / "chainstate", node.chain_path / "chainstate_bak") shutil.copytree(node.chain_path / dir, node.chain_path / f"{dir}_bak")
target_files = list(node.chain_path.glob(file_patt)) target_files = list(node.chain_path.glob(file_patt))
for target_file in target_files: for target_file in target_files:
@ -151,10 +162,9 @@ class InitTest(BitcoinTestFramework):
start_expecting_error(err_fragment) start_expecting_error(err_fragment)
shutil.rmtree(node.chain_path / "blocks") for dir in dirs:
shutil.rmtree(node.chain_path / "chainstate") shutil.rmtree(node.chain_path / dir)
shutil.move(node.chain_path / "blocks_bak", node.chain_path / "blocks") shutil.move(node.chain_path / f"{dir}_bak", node.chain_path / dir)
shutil.move(node.chain_path / "chainstate_bak", node.chain_path / "chainstate")
def init_pid_test(self): def init_pid_test(self):
BITCOIN_PID_FILENAME_CUSTOM = "my_fancy_bitcoin_pid_file.foobar" BITCOIN_PID_FILENAME_CUSTOM = "my_fancy_bitcoin_pid_file.foobar"

View File

@ -45,6 +45,7 @@ from test_framework.util import (
assert_equal, assert_equal,
assert_greater_than, assert_greater_than,
assert_raises_rpc_error, assert_raises_rpc_error,
sync_txindex,
) )
from test_framework.wallet import MiniWallet from test_framework.wallet import MiniWallet
from test_framework.wallet_util import generate_keypair from test_framework.wallet_util import generate_keypair
@ -270,6 +271,7 @@ class MempoolAcceptanceTest(BitcoinTestFramework):
self.log.info('A coinbase transaction') self.log.info('A coinbase transaction')
# Pick the input of the first tx we created, so it has to be a coinbase tx # Pick the input of the first tx we created, so it has to be a coinbase tx
sync_txindex(self, node)
raw_tx_coinbase_spent = node.getrawtransaction(txid=node.decoderawtransaction(hexstring=raw_tx_in_block)['vin'][0]['txid']) raw_tx_coinbase_spent = node.getrawtransaction(txid=node.decoderawtransaction(hexstring=raw_tx_in_block)['vin'][0]['txid'])
tx = tx_from_hex(raw_tx_coinbase_spent) tx = tx_from_hex(raw_tx_coinbase_spent)
self.check_mempool_result( self.check_mempool_result(

View File

@ -34,6 +34,7 @@ from test_framework.util import (
assert_equal, assert_equal,
assert_greater_than, assert_greater_than,
assert_raises_rpc_error, assert_raises_rpc_error,
sync_txindex,
) )
from test_framework.wallet import ( from test_framework.wallet import (
getnewdestination, getnewdestination,
@ -70,7 +71,7 @@ class RawTransactionsTest(BitcoinTestFramework):
self.num_nodes = 3 self.num_nodes = 3
self.extra_args = [ self.extra_args = [
["-txindex"], ["-txindex"],
["-txindex"], [],
["-fastprune", "-prune=1"], ["-fastprune", "-prune=1"],
] ]
# whitelist peers to speed up tx relay / mempool sync # whitelist peers to speed up tx relay / mempool sync
@ -109,6 +110,7 @@ class RawTransactionsTest(BitcoinTestFramework):
self.log.info(f"Test getrawtransaction {'with' if n == 0 else 'without'} -txindex") self.log.info(f"Test getrawtransaction {'with' if n == 0 else 'without'} -txindex")
if n == 0: if n == 0:
sync_txindex(self, self.nodes[n])
# With -txindex. # With -txindex.
# 1. valid parameters - only supply txid # 1. valid parameters - only supply txid
assert_equal(self.nodes[n].getrawtransaction(txId), tx['hex']) assert_equal(self.nodes[n].getrawtransaction(txId), tx['hex'])

View File

@ -12,6 +12,7 @@ from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import ( from test_framework.util import (
assert_equal, assert_equal,
assert_raises_rpc_error, assert_raises_rpc_error,
sync_txindex,
) )
from test_framework.wallet import MiniWallet from test_framework.wallet import MiniWallet
@ -77,6 +78,7 @@ class MerkleBlockTest(BitcoinTestFramework):
assert_equal(sorted(self.nodes[0].verifytxoutproof(self.nodes[0].gettxoutproof([txid1, txid2]))), sorted(txlist)) assert_equal(sorted(self.nodes[0].verifytxoutproof(self.nodes[0].gettxoutproof([txid1, txid2]))), sorted(txlist))
assert_equal(sorted(self.nodes[0].verifytxoutproof(self.nodes[0].gettxoutproof([txid2, txid1]))), sorted(txlist)) assert_equal(sorted(self.nodes[0].verifytxoutproof(self.nodes[0].gettxoutproof([txid2, txid1]))), sorted(txlist))
# We can always get a proof if we have a -txindex # We can always get a proof if we have a -txindex
sync_txindex(self, self.nodes[1])
assert_equal(self.nodes[0].verifytxoutproof(self.nodes[1].gettxoutproof([txid_spent])), [txid_spent]) assert_equal(self.nodes[0].verifytxoutproof(self.nodes[1].gettxoutproof([txid_spent])), [txid_spent])
# We can't get a proof if we specify transactions from different blocks # We can't get a proof if we specify transactions from different blocks
assert_raises_rpc_error(-5, "Not all transactions found in specified or retrieved block", self.nodes[0].gettxoutproof, [txid1, txid3]) assert_raises_rpc_error(-5, "Not all transactions found in specified or retrieved block", self.nodes[0].gettxoutproof, [txid1, txid3])

View File

@ -205,6 +205,8 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
help="Explicitly use v1 transport (can be used to overwrite global --v2transport option)") help="Explicitly use v1 transport (can be used to overwrite global --v2transport option)")
parser.add_argument("--test_methods", dest="test_methods", nargs='*', parser.add_argument("--test_methods", dest="test_methods", nargs='*',
help="Run specified test methods sequentially instead of the full test. Use only for methods that do not depend on any context set up in run_test or other methods.") help="Run specified test methods sequentially instead of the full test. Use only for methods that do not depend on any context set up in run_test or other methods.")
parser.add_argument("--debug_runs", dest="debug_runs", nargs="*", type=int, default=[],
help="Node executions in the test to stall and wait for debugger, 0-based.")
self.add_options(parser) self.add_options(parser)
# Running TestShell in a Jupyter notebook causes an additional -f argument # Running TestShell in a Jupyter notebook causes an additional -f argument
@ -239,6 +241,8 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
PortSeed.n = self.options.port_seed PortSeed.n = self.options.port_seed
TestNode.debug_runs = self.options.debug_runs
def set_binary_paths(self): def set_binary_paths(self):
"""Update self.options with the paths of all binaries from environment variables or their default values""" """Update self.options with the paths of all binaries from environment variables or their default values"""

View File

@ -76,6 +76,9 @@ class TestNode():
To make things easier for the test writer, any unrecognised messages will To make things easier for the test writer, any unrecognised messages will
be dispatched to the RPC connection.""" be dispatched to the RPC connection."""
runs = 0
debug_runs = None
def __init__(self, i, datadir_path, *, chain, rpchost, timewait, timeout_factor, bitcoind, bitcoin_cli, coverage_dir, cwd, extra_conf=None, extra_args=None, use_cli=False, start_perf=False, use_valgrind=False, version=None, descriptors=False, v2transport=False): def __init__(self, i, datadir_path, *, chain, rpchost, timewait, timeout_factor, bitcoind, bitcoin_cli, coverage_dir, cwd, extra_conf=None, extra_args=None, use_cli=False, start_perf=False, use_valgrind=False, version=None, descriptors=False, v2transport=False):
""" """
Kwargs: Kwargs:
@ -253,10 +256,18 @@ class TestNode():
if env is not None: if env is not None:
subp_env.update(env) subp_env.update(env)
wait_for_debugger = TestNode.debug_runs is not None and TestNode.runs in TestNode.debug_runs
if wait_for_debugger:
extra_args.append("-waitfordebugger")
self.process = subprocess.Popen(self.args + extra_args, env=subp_env, stdout=stdout, stderr=stderr, cwd=cwd, **kwargs) self.process = subprocess.Popen(self.args + extra_args, env=subp_env, stdout=stdout, stderr=stderr, cwd=cwd, **kwargs)
self.running = True self.running = True
self.log.debug("bitcoind started, waiting for RPC to come up") if wait_for_debugger:
self.log.info(f"bitcoind started (run #{TestNode.runs}, node #{self.index}), waiting for debugger, PID: {self.process.pid}")
else:
self.log.debug(f"bitcoind started (run #{TestNode.runs}, node #{self.index}), waiting for RPC to come up")
TestNode.runs += 1
if self.start_perf: if self.start_perf:
self._start_perf() self._start_perf()

View File

@ -592,3 +592,10 @@ def find_vout_for_address(node, txid, addr):
if addr == tx["vout"][i]["scriptPubKey"]["address"]: if addr == tx["vout"][i]["scriptPubKey"]["address"]:
return i return i
raise RuntimeError("Vout not found for address: txid=%s, addr=%s" % (txid, addr)) raise RuntimeError("Vout not found for address: txid=%s, addr=%s" % (txid, addr))
def sync_txindex(test_framework, node):
test_framework.log.debug("Waiting for node txindex to sync")
sync_start = int(time.time())
test_framework.wait_until(lambda: node.getindexinfo("txindex")["txindex"]["synced"])
test_framework.log.debug(f"Synced in {time.time() - sync_start} seconds")

View File

@ -117,7 +117,6 @@ class AddressInputTypeGrouping(BitcoinTestFramework):
self.extra_args = [ self.extra_args = [
[ [
"-addresstype=bech32", "-addresstype=bech32",
"-txindex",
], ],
[ [
"-addresstype=p2sh-segwit", "-addresstype=p2sh-segwit",