Inside Bitcoin
Opening the selected file…
Inside Bitcoin
Opening the selected file…
The code behind the network, with room to understand it.
Release published 2026-10-28 · Snapshot fetched 2026-11-01. Latest stable release at fetch time; development branches and Git history are excluded. Only the selected code window and visible folders load here.
501"Should only call stop_node() on a running node after wait_for_rpc_connection() succeeded. "502f"Did you forget to call the latter after start()? Not connected to process: {self.process.pid}")503self.log.debug("Stopping node")504# Do not use wait argument when testing older nodes, e.g. in wallet_backwards_compatibility.py505if self.version_is_at_least(180000):506self.stop(wait=wait)507else:508self.stop()510del self.p2ps[:]512assert (not expected_stderr) or wait_until_stopped # Must wait to check stderr513if wait_until_stopped:514self.wait_until_stopped(expected_stderr=expected_stderr)516def is_node_stopped(self, *, expected_stderr="", expected_ret_code=0):517"""Checks whether the node has stopped.519Returns True if the node has stopped. False otherwise.521If the process has exited, asserts that the exit code matches522`expected_ret_code` (which may be a single value or an iterable of values),523and that stderr matches `expected_stderr` exactly or, if a regex pattern is524provided, contains the pattern.526This method is responsible for freeing resources (self.process)."""527if not self.running:528return True529return_code = self.process.poll()530if return_code is None:531return False533# process has stopped. Assert that it didn't return an error code.534if not isinstance(expected_ret_code, Iterable):535expected_ret_code = (expected_ret_code,)536assert return_code in expected_ret_code, self._node_msg(537f"Node returned unexpected exit code ({return_code}) vs ({expected_ret_code}) when stopping")538# Check that stderr is as expected539self.stderr.seek(0)540stderr = self.stderr.read().decode('utf-8').strip()541if isinstance(expected_stderr, re.Pattern):542if not expected_stderr.search(stderr):543raise AssertionError(f"Unexpected stderr {stderr!r} does not contain {expected_stderr.pattern!r}")544elif stderr != expected_stderr:545raise AssertionError("Unexpected stderr {} != {}".format(stderr, expected_stderr))547self.stdout.close()548self.stderr.close()550self.running = False551self.process = None552self.rpc_connected = False553self._rpc = None554self.log.debug("Node stopped")555return True557def wait_until_stopped(self, *, timeout=BITCOIND_PROC_WAIT_TIMEOUT, expect_error=False, **kwargs):558if "expected_ret_code" not in kwargs:559kwargs["expected_ret_code"] = 1 if expect_error else 0 # Whether node shutdown return EXIT_FAILURE or EXIT_SUCCESS560self.wait_until(lambda: self.is_node_stopped(**kwargs), timeout=timeout)562def kill_process(self):563self.process.kill()564self.wait_until_stopped(expected_ret_code=1 if platform.system() == "Windows" else -9)565assert self.is_node_stopped()567def replace_in_config(self, replacements):568"""569Perform replacements in the configuration file.570The substitutions are passed as a list of search-replace-tuples, e.g.571[("old", "new"), ("foo", "bar"), ...]572"""573with open(self.bitcoinconf, 'r') as conf:574conf_data = conf.read()575for replacement in replacements:576assert_equal(len(replacement), 2)577old, new = replacement[0], replacement[1]578conf_data = conf_data.replace(old, new)579with open(self.bitcoinconf, 'w') as conf:580conf.write(conf_data)582@property583def chain_path(self) -> Path:584return self.datadir_path / self.chain586@property587def debug_log_path(self) -> Path:588return self.chain_path / 'debug.log'590@property591def blocks_path(self) -> Path:592return self.chain_path / "blocks"594@property595def blocks_key_path(self) -> Path:596return self.blocks_path / "xor.dat"598def read_xor_key(self) -> bytes:599with open(self.blocks_key_path, "rb") as xor_f:600return xor_f.read(NUM_XOR_BYTES)602@property603def wallets_path(self) -> Path:604return self.chain_path / "wallets"606def debug_log_size(self, **kwargs) -> int:607with open(self.debug_log_path, **kwargs) as dl:608dl.seek(0, 2)609return dl.tell()611@contextlib.contextmanager612def assert_debug_log(self, expected_msgs, unexpected_msgs=None, *, timeout=0):613if unexpected_msgs is None:614unexpected_msgs = []615assert_equal(type(expected_msgs), list)616assert_equal(type(unexpected_msgs), list)617remaining_expected = list(expected_msgs)619time_end = time.time() + timeout * self.timeout_factor620prev_size = self.debug_log_size(encoding="utf-8") # Must use same encoding that is used to read() below622def join_log(log):623return " - " + "\n - ".join(log.splitlines())625yield627while True:628with open(self.debug_log_path, encoding="utf-8", errors="replace") as dl:629dl.seek(prev_size)630log = dl.read()631for unexpected_msg in unexpected_msgs:632if unexpected_msg in log:633self._raise_assertion_error(f'Unexpected message "{unexpected_msg}" '634f'found in log:\n\n{join_log(log)}\n\n')635while remaining_expected and remaining_expected[-1] in log:636remaining_expected.pop()637if not remaining_expected:638return639if time.time() >= time_end:640break641time.sleep(0.05)642remaining_expected = [e for e in remaining_expected if e not in log]643self._raise_assertion_error(f'Expected message(s) {remaining_expected!s} '644f'not found in log:\n\n{join_log(log)}\n\n')646@contextlib.contextmanager647def busy_wait_for_debug_log(self, expected_msgs, timeout=60):648"""649Block until we see a particular debug log message fragment or until we exceed the timeout.650"""651time_end = time.time() + timeout * self.timeout_factor652prev_size = self.debug_log_size(mode="rb") # Must use same mode that is used to read() below653remaining_expected = list(expected_msgs)655yield657while True:658with open(self.debug_log_path, "rb") as dl:659dl.seek(prev_size)660log = dl.read()662while remaining_expected and remaining_expected[-1] in log:663remaining_expected.pop()664if not remaining_expected:665return667if time.time() >= time_end:668print_log = " - " + "\n - ".join(log.decode("utf8", errors="replace").splitlines())669break671# No sleep here because we want to detect the message fragment as fast as672# possible.674remaining_expected = [e for e in remaining_expected if e not in log]675self._raise_assertion_error(f'Expected message(s) {remaining_expected!s} '676f'not found in log:\n\n{print_log}\n\n')678@contextlib.contextmanager679def wait_for_new_peer(self, timeout=5):680"""681Wait until the node is connected to at least one new peer. We detect this682by watching for an increased highest peer id, using the `getpeerinfo` RPC call.683Note that the simpler approach of only accounting for the number of peers684suffers from race conditions, as disconnects from unrelated previous peers685could happen anytime in-between.686"""687def get_highest_peer_id():688peer_info = self.getpeerinfo()689return peer_info[-1]["id"] if peer_info else -1691initial_peer_id = get_highest_peer_id()692yield693self.wait_until(lambda: get_highest_peer_id() > initial_peer_id, timeout=timeout)695def assert_start_raises_init_error(self, extra_args=None, expected_msg=None, match=ErrorMatch.FULL_TEXT, *args, **kwargs):696"""Attempt to start the node and expect it to raise an error.698extra_args: extra arguments to pass through to bitcoind699expected_msg: regex that stderr should match when bitcoind fails701Will raise if bitcoind starts without an error.702Will raise if an expected_msg is provided and it does not match bitcoind's stdout."""703assert not self.running704with tempfile.NamedTemporaryFile(dir=self.stderr_dir, delete=False) as log_stderr, \705tempfile.NamedTemporaryFile(dir=self.stdout_dir, delete=False) as log_stdout:706assert_msg = None707try:708self.start(extra_args, stdout=log_stdout, stderr=log_stderr, *args, **kwargs)709ret = self.process.wait(timeout=self.rpc_timeout)710self.log.debug(self._node_msg(f'bitcoind exited with status {ret} during initialization'))711assert_not_equal(ret, 0) # Exit code must indicate failure712self.running = False713self.process = None714# Check stderr for expected message715if expected_msg is not None:716log_stderr.seek(0)717stderr = log_stderr.read().decode('utf-8').strip()718if match == ErrorMatch.PARTIAL_REGEX:719if re.search(expected_msg, stderr, flags=re.MULTILINE) is None:720self._raise_assertion_error(721'Expected message "{}" does not partially match stderr:\n"{}"'.format(expected_msg, stderr))722elif match == ErrorMatch.FULL_REGEX:723if re.fullmatch(expected_msg, stderr) is None:724self._raise_assertion_error(725'Expected message "{}" does not fully match stderr:\n"{}"'.format(expected_msg, stderr))726elif match == ErrorMatch.FULL_TEXT:727if expected_msg != stderr:728self._raise_assertion_error(729'Expected message "{}" does not fully match stderr:\n"{}"'.format(expected_msg, stderr))730except subprocess.TimeoutExpired as e:731self.process.kill()732self.running = False733self.process = None734assert_msg = f'bitcoind should have exited within {self.rpc_timeout}s '735if expected_msg is None:736assert_msg += "with an error"737else:738assert_msg += "with expected error " + expected_msg739assert_msg += f" (cmd: {e.cmd})"741# Raise assertion outside of except-block above in order for it not to be treated as a knock-on exception.742if assert_msg:743self._raise_assertion_error(assert_msg)745def add_p2p_connection(self, p2p_conn, *, wait_for_verack=True, send_version=True, supports_v2_p2p=None, wait_for_v2_handshake=True, expect_success=True, **kwargs):746"""Add an inbound p2p connection to the node.748This method adds the p2p connection to the self.p2ps list and also749returns the connection to the caller.
Independent explanations by MyBitcoinExplorer. Not affiliated with or endorsed by Bitcoin Core. Guides are educational and do not replace the source or its tests.
Bitcoin Core source © its contributors, distributed under the MIT license. Original notices and bundled third-party licenses are retained in the source tree.