1 #!/usr/bin/env python3
2 # Copyright (c) 2014-present The Limenka developers
3 # Distributed under the MIT software license, see the accompanying
4 # file COPYING or http://www.opensource.org/licenses/mit-license.php.
5 """Base class for RPC testing."""
6 7 import configparser
8 from enum import Enum
9 import argparse
10 from importlib.util import find_spec
11 import logging
12 import os
13 import platform
14 import pdb
15 import random
16 import re
17 import shutil
18 import subprocess
19 import sys
20 import tempfile
21 import time
22 23 from .address import create_deterministic_address_bcrt1_p2tr_op_true
24 from .authproxy import JSONRPCException
25 from . import coverage
26 from .p2p import NetworkThread
27 from .test_node import TestNode
28 from .util import (
29 MAX_NODES,
30 PortSeed,
31 assert_equal,
32 check_json_precision,
33 find_vout_for_address,
34 get_datadir_path,
35 initialize_datadir,
36 p2p_port,
37 wait_until_helper_internal,
38 )
39 40 41 class TestStatus(Enum):
42 PASSED = 1
43 FAILED = 2
44 SKIPPED = 3
45 46 TEST_EXIT_PASSED = 0
47 TEST_EXIT_FAILED = 1
48 TEST_EXIT_SKIPPED = 77
49 50 TMPDIR_PREFIX = "limenka_func_test_"
51 52 53 class SkipTest(Exception):
54 """This exception is raised to skip a test"""
55 56 def __init__(self, message):
57 self.message = message
58 59 60 class LimenkaTestMetaClass(type):
61 """Metaclass for LimenkaTestFramework.
62 63 Ensures that any attempt to register a subclass of `LimenkaTestFramework`
64 adheres to a standard whereby the subclass overrides `set_test_params` and
65 `run_test` but DOES NOT override either `__init__` or `main`. If any of
66 those standards are violated, a ``TypeError`` is raised."""
67 68 def __new__(cls, clsname, bases, dct):
69 if not clsname == 'LimenkaTestFramework':
70 if not ('run_test' in dct and 'set_test_params' in dct):
71 raise TypeError("LimenkaTestFramework subclasses must override "
72 "'run_test' and 'set_test_params'")
73 if '__init__' in dct or 'main' in dct:
74 raise TypeError("LimenkaTestFramework subclasses may not override "
75 "'__init__' or 'main'")
76 77 return super().__new__(cls, clsname, bases, dct)
78 79 80 class LimenkaTestFramework(metaclass=LimenkaTestMetaClass):
81 """Base class for a limenka test script.
82 83 Individual limenka test scripts should subclass this class and override the set_test_params() and run_test() methods.
84 85 Individual tests can also override the following methods to customize the test setup:
86 87 - add_options()
88 - setup_chain()
89 - setup_network()
90 - setup_nodes()
91 92 The __init__() and main() methods should not be overridden.
93 94 This class also contains various public and private helper methods."""
95 96 def __init__(self, test_file) -> None:
97 """Sets test framework defaults. Do not override this method. Instead, override the set_test_params() method"""
98 self.chain: str = 'regtest'
99 self.setup_clean_chain: bool = False
100 self.noban_tx_relay: bool = False
101 self.nodes: list[TestNode] = []
102 self.extra_args = None
103 self.network_thread = None
104 self.rpc_timeout = 60 # Wait for up to 60 seconds for the RPC server to respond
105 self.supports_cli = True
106 self.bind_to_localhost_only = True
107 self.parse_args(test_file)
108 self.default_wallet_name = "default_wallet" if self.options.descriptors else ""
109 self.wallet_data_filename = "wallet.dat"
110 # Optional list of wallet names that can be set in set_test_params to
111 # create and import keys to. If unset, default is len(nodes) *
112 # [default_wallet_name]. If wallet names are None, wallet creation is
113 # skipped. If list is truncated, wallet creation is skipped and keys
114 # are not imported.
115 self.wallet_names = None
116 # By default the wallet is not required. Set to true by skip_if_no_wallet().
117 # When False, we ignore wallet_names regardless of what it is.
118 self._requires_wallet = False
119 # Disable ThreadOpenConnections by default, so that adding entries to
120 # addrman will not result in automatic connections to them.
121 self.disable_autoconnect = True
122 self.set_test_params()
123 assert self.wallet_names is None or len(self.wallet_names) <= self.num_nodes
124 self.rpc_timeout = int(self.rpc_timeout * self.options.timeout_factor) # optionally, increase timeout by a factor
125 126 def main(self):
127 """Main function. This should not be overridden by the subclass test scripts."""
128 129 assert hasattr(self, "num_nodes"), "Test must set self.num_nodes in set_test_params()"
130 131 try:
132 self.setup()
133 if self.options.test_methods:
134 self.run_test_methods()
135 else:
136 self.run_test()
137 138 except SkipTest as e:
139 self.log.warning("Test Skipped: %s" % e.message)
140 self.success = TestStatus.SKIPPED
141 except subprocess.CalledProcessError as e:
142 self.log.exception(f"Called Process failed with stdout='{e.stdout}'; stderr='{e.stderr}';")
143 self.success = TestStatus.FAILED
144 except BaseException:
145 self.log.exception("Unexpected exception")
146 self.success = TestStatus.FAILED
147 finally:
148 exit_code = self.shutdown()
149 sys.exit(exit_code)
150 151 def run_test_methods(self):
152 for method_name in self.options.test_methods:
153 self.log.info(f"Attempting to execute method: {method_name}")
154 method = getattr(self, method_name)
155 method()
156 self.log.info(f"Method '{method_name}' executed successfully.")
157 158 def parse_args(self, test_file):
159 previous_releases_path = os.getenv("PREVIOUS_RELEASES_DIR") or os.getcwd() + "/releases"
160 parser = argparse.ArgumentParser(usage="%(prog)s [options]")
161 parser.add_argument("--nocleanup", dest="nocleanup", default=False, action="store_true",
162 help="Leave limenkads and test.* datadir on exit or error")
163 parser.add_argument("--cachedir", dest="cachedir", default=os.path.abspath(os.path.dirname(test_file) + "/../cache"),
164 help="Directory for caching pregenerated datadirs (default: %(default)s)")
165 parser.add_argument("--tmpdir", dest="tmpdir", help="Root directory for datadirs (must not exist)")
166 parser.add_argument("-l", "--loglevel", dest="loglevel", default="INFO",
167 help="log events at this level and higher to the console. Can be set to DEBUG, INFO, WARNING, ERROR or CRITICAL. Passing --loglevel DEBUG will output all logs to console. Note that logs at all levels are always written to the test_framework.log file in the temporary test directory.")
168 parser.add_argument("--tracerpc", dest="trace_rpc", default=False, action="store_true",
169 help="Print out all RPC calls as they are made")
170 parser.add_argument("--portseed", dest="port_seed", default=os.getpid(), type=int,
171 help="The seed to use for assigning port numbers (default: current process id)")
172 parser.add_argument("--previous-releases", dest="prev_releases", action="store_true",
173 default=os.path.isdir(previous_releases_path) and bool(os.listdir(previous_releases_path)),
174 help="Force test of previous releases (default: %(default)s)")
175 parser.add_argument("--coveragedir", dest="coveragedir",
176 help="Write tested RPC commands into this directory")
177 parser.add_argument("--configfile", dest="configfile",
178 default=os.path.abspath(os.path.dirname(test_file) + "/../config.ini"),
179 help="Location of the test framework config file (default: %(default)s)")
180 parser.add_argument("--pdbonfailure", dest="pdbonfailure", default=False, action="store_true",
181 help="Attach a python debugger if test fails")
182 parser.add_argument("--usecli", dest="usecli", default=False, action="store_true",
183 help="use limenka-cli instead of RPC for all commands")
184 parser.add_argument("--perf", dest="perf", default=False, action="store_true",
185 help="profile running nodes with perf for the duration of the test")
186 parser.add_argument("--valgrind", dest="valgrind", default=False, action="store_true",
187 help="run nodes under the valgrind memory error detector: expect at least a ~10x slowdown. valgrind 3.14 or later required. Does not apply to previous release binaries.")
188 parser.add_argument("--randomseed", type=int,
189 help="set a random seed for deterministically reproducing a previous test run")
190 parser.add_argument("--timeout-factor", dest="timeout_factor", type=float, help="adjust test timeouts by a factor. Setting it to 0 disables all timeouts")
191 parser.add_argument("--v2transport", dest="v2transport", default=False, action="store_true",
192 help="use BIP324 v2 connections between all nodes by default")
193 parser.add_argument("--v1transport", dest="v1transport", default=False, action="store_true",
194 help="Explicitly use v1 transport (can be used to overwrite global --v2transport option)")
195 parser.add_argument("--test_methods", dest="test_methods", nargs='*',
196 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.")
197 198 self.add_options(parser)
199 # Running TestShell in a Jupyter notebook causes an additional -f argument
200 # To keep TestShell from failing with an "unrecognized argument" error, we add a dummy "-f" argument
201 # source: https://stackoverflow.com/questions/48796169/how-to-fix-ipykernel-launcher-py-error-unrecognized-arguments-in-jupyter/56349168#56349168
202 parser.add_argument("-f", "--fff", help="a dummy argument to fool ipython", default="1")
203 self.options = parser.parse_args()
204 if self.options.timeout_factor == 0:
205 self.options.timeout_factor = 99999
206 self.options.timeout_factor = self.options.timeout_factor or (4 if self.options.valgrind else 1)
207 self.options.previous_releases_path = previous_releases_path
208 209 config = configparser.ConfigParser()
210 config.read_file(open(self.options.configfile))
211 self.config = config
212 if self.options.v1transport:
213 self.options.v2transport=False
214 215 if "descriptors" not in self.options:
216 # Wallet is not required by the test at all and the value of self.options.descriptors won't matter.
217 # It still needs to exist and be None in order for tests to work however.
218 # So set it to None to force -disablewallet, because the wallet is not needed.
219 self.options.descriptors = None
220 elif self.options.descriptors is None:
221 # Some wallet is either required or optionally used by the test.
222 # Prefer SQLite unless it isn't available
223 if self.is_sqlite_compiled():
224 self.options.descriptors = True
225 elif self.is_bdb_compiled():
226 self.options.descriptors = False
227 else:
228 # If neither are compiled, tests requiring a wallet will be skipped and the value of self.options.descriptors won't matter
229 # It still needs to exist and be None in order for tests to work however.
230 # So set it to None, which will also set -disablewallet.
231 self.options.descriptors = None
232 233 PortSeed.n = self.options.port_seed
234 235 def set_binary_paths(self):
236 """Update self.options with the paths of all binaries from environment variables or their default values"""
237 238 binaries = {
239 "limenkad": ("limenkad", "LIMENKAD"),
240 "limenka-cli": ("limenkacli", "LIMENKACLI"),
241 "limenka-util": ("limenkautil", "LIMENKAUTIL"),
242 "limenka-wallet": ("limenkawallet", "LIMENKAWALLET"),
243 }
244 for binary, [attribute_name, env_variable_name] in binaries.items():
245 default_filename = os.path.join(
246 self.config["environment"]["BUILDDIR"],
247 "bin",
248 binary + self.config["environment"]["EXEEXT"],
249 )
250 setattr(self.options, attribute_name, os.getenv(env_variable_name, default=default_filename))
251 252 def setup(self):
253 """Call this method to start up the test framework object with options set."""
254 255 check_json_precision()
256 257 self.options.cachedir = os.path.abspath(self.options.cachedir)
258 259 config = self.config
260 261 self.set_binary_paths()
262 263 os.environ['PATH'] = os.pathsep.join([
264 os.path.join(config['environment']['BUILDDIR'], 'bin'),
265 os.environ['PATH']
266 ])
267 268 # Set up temp directory and start logging
269 if self.options.tmpdir:
270 self.options.tmpdir = os.path.abspath(self.options.tmpdir)
271 os.makedirs(self.options.tmpdir, exist_ok=False)
272 else:
273 self.options.tmpdir = tempfile.mkdtemp(prefix=TMPDIR_PREFIX)
274 self._start_logging()
275 276 # Seed the PRNG. Note that test runs are reproducible if and only if
277 # a single thread accesses the PRNG. For more information, see
278 # https://docs.python.org/3/library/random.html#notes-on-reproducibility.
279 # The network thread shouldn't access random. If we need to change the
280 # network thread to access randomness, it should instantiate its own
281 # random.Random object.
282 seed = self.options.randomseed
283 284 if seed is None:
285 seed = random.randrange(sys.maxsize)
286 else:
287 self.log.info("User supplied random seed {}".format(seed))
288 289 random.seed(seed)
290 self.log.info("PRNG seed is: {}".format(seed))
291 292 self.log.debug('Setting up network thread')
293 self.network_thread = NetworkThread()
294 self.network_thread.start()
295 296 if self.options.usecli:
297 if not self.supports_cli:
298 raise SkipTest("--usecli specified but test does not support using CLI")
299 self.skip_if_no_cli()
300 self.skip_test_if_missing_module()
301 self.setup_chain()
302 self.setup_network()
303 304 self.success = TestStatus.PASSED
305 306 def shutdown(self):
307 """Call this method to shut down the test framework object."""
308 309 if self.success == TestStatus.FAILED and self.options.pdbonfailure:
310 print("Testcase failed. Attaching python debugger. Enter ? for help")
311 pdb.set_trace()
312 313 self.log.debug('Closing down network thread')
314 self.network_thread.close(timeout=self.options.timeout_factor * 10)
315 if self.success == TestStatus.FAILED:
316 self.log.info("Not stopping nodes as test failed. The dangling processes will be cleaned up later.")
317 else:
318 self.log.info("Stopping nodes")
319 if self.nodes:
320 self.stop_nodes()
321 322 should_clean_up = (
323 not self.options.nocleanup and
324 self.success != TestStatus.FAILED and
325 not self.options.perf
326 )
327 if should_clean_up:
328 self.log.info("Cleaning up {} on exit".format(self.options.tmpdir))
329 cleanup_tree_on_exit = True
330 elif self.options.perf:
331 self.log.warning("Not cleaning up dir {} due to perf data".format(self.options.tmpdir))
332 cleanup_tree_on_exit = False
333 else:
334 self.log.warning("Not cleaning up dir {}".format(self.options.tmpdir))
335 cleanup_tree_on_exit = False
336 337 if self.success == TestStatus.PASSED:
338 self.log.info("Tests successful")
339 exit_code = TEST_EXIT_PASSED
340 elif self.success == TestStatus.SKIPPED:
341 self.log.info("Test skipped")
342 exit_code = TEST_EXIT_SKIPPED
343 else:
344 self.log.error("Test failed. Test logging available at %s/test_framework.log", self.options.tmpdir)
345 self.log.error("")
346 self.log.error("Hint: Call {} '{}' to consolidate all logs".format(os.path.normpath(os.path.dirname(os.path.realpath(__file__)) + "/../combine_logs.py"), self.options.tmpdir))
347 self.log.error("")
348 self.log.error("If this failure happened unexpectedly or intermittently, please file a bug and provide a link or upload of the combined log.")
349 self.log.error(self.config['environment']['CLIENT_BUGREPORT'])
350 self.log.error("")
351 exit_code = TEST_EXIT_FAILED
352 # Logging.shutdown will not remove stream- and filehandlers, so we must
353 # do it explicitly. Handlers are removed so the next test run can apply
354 # different log handler settings.
355 # See: https://docs.python.org/3/library/logging.html#logging.shutdown
356 for h in list(self.log.handlers):
357 h.flush()
358 h.close()
359 self.log.removeHandler(h)
360 rpc_logger = logging.getLogger("LimenkaRPC")
361 for h in list(rpc_logger.handlers):
362 h.flush()
363 rpc_logger.removeHandler(h)
364 if cleanup_tree_on_exit:
365 shutil.rmtree(self.options.tmpdir)
366 367 self.nodes.clear()
368 return exit_code
369 370 # Methods to override in subclass test scripts.
371 def set_test_params(self):
372 """Tests must override this method to change default values for number of nodes, topology, etc"""
373 raise NotImplementedError
374 375 def add_options(self, parser):
376 """Override this method to add command-line options to the test"""
377 pass
378 379 def skip_test_if_missing_module(self):
380 """Override this method to skip a test if a module is not compiled"""
381 pass
382 383 def setup_chain(self):
384 """Override this method to customize blockchain setup"""
385 self.log.info("Initializing test directory " + self.options.tmpdir)
386 if self.setup_clean_chain:
387 self._initialize_chain_clean()
388 else:
389 self._initialize_chain()
390 391 def setup_network(self):
392 """Override this method to customize test network topology"""
393 self.setup_nodes()
394 395 # Connect the nodes as a "chain". This allows us
396 # to split the network between nodes 1 and 2 to get
397 # two halves that can work on competing chains.
398 #
399 # Topology looks like this:
400 # node0 <-- node1 <-- node2 <-- node3
401 #
402 # If all nodes are in IBD (clean chain from genesis), node0 is assumed to be the source of blocks (miner). To
403 # ensure block propagation, all nodes will establish outgoing connections toward node0.
404 # See fPreferredDownload in net_processing.
405 #
406 # If further outbound connections are needed, they can be added at the beginning of the test with e.g.
407 # self.connect_nodes(1, 2)
408 for i in range(self.num_nodes - 1):
409 self.connect_nodes(i + 1, i)
410 self.sync_all()
411 412 def setup_nodes(self):
413 """Override this method to customize test node setup"""
414 self.add_nodes(self.num_nodes, self.extra_args)
415 self.start_nodes()
416 if self._requires_wallet:
417 self.import_deterministic_coinbase_privkeys()
418 if not self.setup_clean_chain:
419 for n in self.nodes:
420 assert_equal(n.getblockchaininfo()["blocks"], 199)
421 # To ensure that all nodes are out of IBD, the most recent block
422 # must have a timestamp not too old (see IsInitialBlockDownload()).
423 self.log.debug('Generate a block with current time')
424 block_hash = self.generate(self.nodes[0], 1, sync_fun=self.no_op)[0]
425 block = self.nodes[0].getblock(blockhash=block_hash, verbosity=0)
426 for n in self.nodes:
427 n.submitblock(block)
428 chain_info = n.getblockchaininfo()
429 assert_equal(chain_info["blocks"], 200)
430 assert_equal(chain_info["initialblockdownload"], False)
431 432 def import_deterministic_coinbase_privkeys(self):
433 for i in range(self.num_nodes):
434 self.init_wallet(node=i)
435 436 def init_wallet(self, *, node):
437 wallet_name = self.default_wallet_name if self.wallet_names is None else self.wallet_names[node] if node < len(self.wallet_names) else False
438 if wallet_name is not False:
439 n = self.nodes[node]
440 if wallet_name is not None:
441 n.createwallet(wallet_name=wallet_name, descriptors=self.options.descriptors, load_on_startup=True)
442 n.importprivkey(privkey=n.get_deterministic_priv_key().key, label='coinbase', rescan=True)
443 444 # Only enables wallet support when the module is available
445 def enable_wallet_if_possible(self):
446 self._requires_wallet = self.is_wallet_compiled()
447 448 def run_test(self):
449 """Tests must override this method to define test logic"""
450 raise NotImplementedError
451 452 # Public helper methods. These can be accessed by the subclass test scripts.
453 454 def add_wallet_options(self, parser, *, descriptors=True, legacy=True):
455 kwargs = {}
456 if descriptors + legacy == 1:
457 # If only one type can be chosen, set it as default
458 kwargs["default"] = descriptors
459 group = parser.add_mutually_exclusive_group(
460 # If only one type is allowed, require it to be set in test_runner.py
461 required=os.getenv("REQUIRE_WALLET_TYPE_SET") == "1" and "default" in kwargs)
462 if descriptors:
463 group.add_argument("--descriptors", action='store_const', const=True, **kwargs,
464 help="Run test using a descriptor wallet", dest='descriptors')
465 if legacy:
466 group.add_argument("--legacy-wallet", action='store_const', const=False, **kwargs,
467 help="Run test using legacy wallets", dest='descriptors')
468 469 def add_nodes(self, num_nodes: int, extra_args=None, *, rpchost=None, binary=None, binary_cli=None, versions=None):
470 """Instantiate TestNode objects.
471 472 Should only be called once after the nodes have been specified in
473 set_test_params()."""
474 def get_bin_from_version(version, bin_name, bin_default):
475 if not version:
476 return bin_default
477 if version > 219999:
478 # Starting at client version 220000 the first two digits represent
479 # the major version, e.g. v22.0 instead of v0.22.0.
480 version *= 100
481 return os.path.join(
482 self.options.previous_releases_path,
483 re.sub(
484 r'\.0$' if version <= 219999 else r'(\.0){1,2}$',
485 '', # Remove trailing dot for point releases, after 22.0 also remove double trailing dot.
486 'v{}.{}.{}.{}'.format(
487 (version % 100000000) // 1000000,
488 (version % 1000000) // 10000,
489 (version % 10000) // 100,
490 (version % 100) // 1,
491 ),
492 ),
493 'bin',
494 bin_name,
495 )
496 497 if self.bind_to_localhost_only:
498 extra_confs = [["bind=127.0.0.1"]] * num_nodes
499 else:
500 extra_confs = [[]] * num_nodes
501 if extra_args is None:
502 extra_args = [[]] * num_nodes
503 # Whitelist peers to speed up tx relay / mempool sync. Don't use it if testing tx relay or timing.
504 if self.noban_tx_relay:
505 for i in range(len(extra_args)):
506 extra_args[i] = extra_args[i] + ["-whitelist=noban,in,out@127.0.0.1"]
507 if versions is None:
508 versions = [None] * num_nodes
509 if binary is None:
510 binary = [get_bin_from_version(v, 'limenkad', self.options.limenkad) for v in versions]
511 if binary_cli is None:
512 binary_cli = [get_bin_from_version(v, 'limenka-cli', self.options.limenkacli) for v in versions]
513 # Fail test if any of the needed release binaries is missing
514 bins_missing = False
515 for bin_path in binary + binary_cli:
516 if shutil.which(bin_path) is None:
517 self.log.error(f"Binary not found: {bin_path}")
518 bins_missing = True
519 if bins_missing:
520 raise AssertionError("At least one release binary is missing. "
521 "Previous releases binaries can be downloaded via `test/get_previous_releases.py -b`.")
522 assert_equal(len(extra_confs), num_nodes)
523 assert_equal(len(extra_args), num_nodes)
524 assert_equal(len(versions), num_nodes)
525 assert_equal(len(binary), num_nodes)
526 assert_equal(len(binary_cli), num_nodes)
527 for i in range(num_nodes):
528 args = list(extra_args[i])
529 test_node_i = TestNode(
530 i,
531 get_datadir_path(self.options.tmpdir, i),
532 chain=self.chain,
533 rpchost=rpchost,
534 timewait=self.rpc_timeout,
535 timeout_factor=self.options.timeout_factor,
536 limenkad=binary[i],
537 limenka_cli=binary_cli[i],
538 version=versions[i],
539 coverage_dir=self.options.coveragedir,
540 cwd=self.options.tmpdir,
541 extra_conf=extra_confs[i],
542 extra_args=args,
543 use_cli=self.options.usecli,
544 start_perf=self.options.perf,
545 use_valgrind=self.options.valgrind,
546 descriptors=self.options.descriptors,
547 v2transport=self.options.v2transport,
548 )
549 self.nodes.append(test_node_i)
550 if not test_node_i.version_is_at_least(170000):
551 # adjust conf for pre 17
552 test_node_i.replace_in_config([('[regtest]', '')])
553 554 def start_node(self, i, *args, **kwargs):
555 """Start a limenkad"""
556 557 node = self.nodes[i]
558 559 node.start(*args, **kwargs)
560 node.wait_for_rpc_connection()
561 562 if self.options.coveragedir is not None:
563 coverage.write_all_rpc_commands(self.options.coveragedir, node.rpc)
564 565 def start_nodes(self, extra_args=None, *args, **kwargs):
566 """Start multiple limenkads"""
567 568 if extra_args is None:
569 extra_args = [None] * self.num_nodes
570 assert_equal(len(extra_args), self.num_nodes)
571 for i, node in enumerate(self.nodes):
572 node.start(extra_args[i], *args, **kwargs)
573 for node in self.nodes:
574 node.wait_for_rpc_connection()
575 576 if self.options.coveragedir is not None:
577 for node in self.nodes:
578 coverage.write_all_rpc_commands(self.options.coveragedir, node.rpc)
579 580 def stop_node(self, i, expected_stderr='', wait=0):
581 """Stop a limenkad test node"""
582 self.nodes[i].stop_node(expected_stderr, wait=wait)
583 584 def stop_nodes(self, wait=0):
585 """Stop multiple limenkad test nodes"""
586 for node in self.nodes:
587 # Issue RPC to stop nodes
588 node.stop_node(wait=wait, wait_until_stopped=False)
589 590 for node in self.nodes:
591 # Wait for nodes to stop
592 node.wait_until_stopped()
593 594 def restart_node(self, i, extra_args=None, clear_addrman=False):
595 """Stop and start a test node"""
596 self.stop_node(i)
597 if clear_addrman:
598 peers_dat = self.nodes[i].chain_path / "peers.dat"
599 os.remove(peers_dat)
600 with self.nodes[i].assert_debug_log(expected_msgs=[f'Creating peers.dat because the file was not found ("{peers_dat}")']):
601 self.start_node(i, extra_args)
602 else:
603 self.start_node(i, extra_args)
604 605 def wait_for_node_exit(self, i, timeout):
606 self.nodes[i].process.wait(timeout)
607 608 def connect_nodes(self, a, b, *, peer_advertises_v2=None, wait_for_connect: bool = True):
609 """
610 Kwargs:
611 wait_for_connect: if True, block until the nodes are verified as connected. You might
612 want to disable this when using -stopatheight with one of the connected nodes,
613 since there will be a race between the actual connection and performing
614 the assertions before one node shuts down.
615 """
616 from_connection = self.nodes[a]
617 to_connection = self.nodes[b]
618 ip_port = "127.0.0.1:" + str(p2p_port(b))
619 620 if peer_advertises_v2 is None:
621 peer_advertises_v2 = from_connection.use_v2transport
622 623 if peer_advertises_v2 != from_connection.use_v2transport:
624 from_connection.addnode(node=ip_port, command="onetry", v2transport=peer_advertises_v2)
625 else:
626 # skip the optional third argument if it matches the default, for
627 # compatibility with older clients
628 from_connection.addnode(ip_port, "onetry")
629 630 if not wait_for_connect:
631 return
632 633 # Use subversion as peer id. Test nodes have their node number appended to the user agent string
634 from_connection_subver = from_connection.getnetworkinfo()['subversion']
635 to_connection_subver = to_connection.getnetworkinfo()['subversion']
636 637 def find_conn(node, peer_subversion, inbound):
638 return next(filter(lambda peer: peer['subver'] == peer_subversion and peer['inbound'] == inbound, node.getpeerinfo()), None)
639 640 self.wait_until(lambda: find_conn(from_connection, to_connection_subver, inbound=False) is not None)
641 self.wait_until(lambda: find_conn(to_connection, from_connection_subver, inbound=True) is not None)
642 643 def check_bytesrecv(peer, msg_type, min_bytes_recv):
644 assert peer is not None, "Error: peer disconnected"
645 return peer['bytesrecv_per_msg'].pop(msg_type, 0) >= min_bytes_recv
646 647 # Poll until version handshake (fSuccessfullyConnected) is complete to
648 # avoid race conditions, because some message types are blocked from
649 # being sent or received before fSuccessfullyConnected.
650 #
651 # As the flag fSuccessfullyConnected is not exposed, check it by
652 # waiting for a pong, which can only happen after the flag was set.
653 self.wait_until(lambda: check_bytesrecv(find_conn(from_connection, to_connection_subver, inbound=False), 'pong', 29))
654 self.wait_until(lambda: check_bytesrecv(find_conn(to_connection, from_connection_subver, inbound=True), 'pong', 29))
655 656 def disconnect_nodes(self, a, b):
657 def disconnect_nodes_helper(node_a, node_b):
658 def get_peer_ids(from_connection, node_num):
659 result = []
660 for peer in from_connection.getpeerinfo():
661 if "testnode{}".format(node_num) in peer['subver']:
662 result.append(peer['id'])
663 return result
664 665 peer_ids = get_peer_ids(node_a, node_b.index)
666 if not peer_ids:
667 self.log.warning("disconnect_nodes: {} and {} were not connected".format(
668 node_a.index,
669 node_b.index,
670 ))
671 return
672 for peer_id in peer_ids:
673 try:
674 node_a.disconnectnode(nodeid=peer_id)
675 except JSONRPCException as e:
676 # If this node is disconnected between calculating the peer id
677 # and issuing the disconnect, don't worry about it.
678 # This avoids a race condition if we're mass-disconnecting peers.
679 if e.error['code'] != -29: # RPC_CLIENT_NODE_NOT_CONNECTED
680 raise
681 682 # wait to disconnect
683 self.wait_until(lambda: not get_peer_ids(node_a, node_b.index), timeout=5)
684 self.wait_until(lambda: not get_peer_ids(node_b, node_a.index), timeout=5)
685 686 disconnect_nodes_helper(self.nodes[a], self.nodes[b])
687 688 def split_network(self):
689 """
690 Split the network of four nodes into nodes 0/1 and 2/3.
691 """
692 self.disconnect_nodes(1, 2)
693 self.sync_all(self.nodes[:2])
694 self.sync_all(self.nodes[2:])
695 696 def join_network(self):
697 """
698 Join the (previously split) network halves together.
699 """
700 self.connect_nodes(1, 2)
701 self.sync_all()
702 703 def no_op(self):
704 pass
705 706 def generate(self, generator, *args, sync_fun=None, **kwargs):
707 blocks = generator.generate(*args, called_by_framework=True, **kwargs)
708 sync_fun() if sync_fun else self.sync_all()
709 return blocks
710 711 def generateblock(self, generator, *args, sync_fun=None, **kwargs):
712 blocks = generator.generateblock(*args, called_by_framework=True, **kwargs)
713 sync_fun() if sync_fun else self.sync_all()
714 return blocks
715 716 def generatetoaddress(self, generator, *args, sync_fun=None, **kwargs):
717 blocks = generator.generatetoaddress(*args, called_by_framework=True, **kwargs)
718 sync_fun() if sync_fun else self.sync_all()
719 return blocks
720 721 def generatetodescriptor(self, generator, *args, sync_fun=None, **kwargs):
722 blocks = generator.generatetodescriptor(*args, called_by_framework=True, **kwargs)
723 sync_fun() if sync_fun else self.sync_all()
724 return blocks
725 726 def create_outpoints(self, node, *, outputs):
727 """Send funds to a given list of `{address: amount}` targets using the limenkad
728 wallet and return the corresponding outpoints as a list of dictionaries
729 `[{"txid": txid, "vout": vout1}, {"txid": txid, "vout": vout2}, ...]`.
730 The result can be used to specify inputs for RPCs like `createrawtransaction`,
731 `createpsbt`, `lockunspent` etc."""
732 assert all(len(output.keys()) == 1 for output in outputs)
733 send_res = node.send(outputs)
734 assert send_res["complete"]
735 utxos = []
736 for output in outputs:
737 address = list(output.keys())[0]
738 vout = find_vout_for_address(node, send_res["txid"], address)
739 utxos.append({"txid": send_res["txid"], "vout": vout})
740 return utxos
741 742 def sync_blocks(self, nodes=None, wait=1, timeout=60):
743 """
744 Wait until everybody has the same tip.
745 sync_blocks needs to be called with an rpc_connections set that has least
746 one node already synced to the latest, stable tip, otherwise there's a
747 chance it might return before all nodes are stably synced.
748 """
749 rpc_connections = nodes or self.nodes
750 timeout = int(timeout * self.options.timeout_factor)
751 stop_time = time.time() + timeout
752 while time.time() <= stop_time:
753 best_hash = [x.getbestblockhash() for x in rpc_connections]
754 if best_hash.count(best_hash[0]) == len(rpc_connections):
755 return
756 # Check that each peer has at least one connection
757 assert (all([len(x.getpeerinfo()) for x in rpc_connections]))
758 time.sleep(wait)
759 raise AssertionError("Block sync timed out after {}s:{}".format(
760 timeout,
761 "".join("\n {!r}".format(b) for b in best_hash),
762 ))
763 764 def sync_mempools(self, nodes=None, wait=1, timeout=60, flush_scheduler=True):
765 """
766 Wait until everybody has the same transactions in their memory
767 pools
768 """
769 rpc_connections = nodes or self.nodes
770 timeout = int(timeout * self.options.timeout_factor)
771 stop_time = time.time() + timeout
772 while time.time() <= stop_time:
773 pool = [set(r.getrawmempool()) for r in rpc_connections]
774 if pool.count(pool[0]) == len(rpc_connections):
775 if flush_scheduler:
776 for r in rpc_connections:
777 r.syncwithvalidationinterfacequeue()
778 return
779 # Check that each peer has at least one connection
780 assert (all([len(x.getpeerinfo()) for x in rpc_connections]))
781 time.sleep(wait)
782 raise AssertionError("Mempool sync timed out after {}s:{}".format(
783 timeout,
784 "".join("\n {!r}".format(m) for m in pool),
785 ))
786 787 def sync_all(self, nodes=None):
788 self.sync_blocks(nodes)
789 self.sync_mempools(nodes)
790 791 def wait_until(self, test_function, timeout=60, check_interval=0.05):
792 return wait_until_helper_internal(test_function, timeout=timeout, timeout_factor=self.options.timeout_factor, check_interval=check_interval)
793 794 # Private helper methods. These should not be accessed by the subclass test scripts.
795 796 def _start_logging(self):
797 # Add logger and logging handlers
798 self.log = logging.getLogger('TestFramework')
799 self.log.setLevel(logging.DEBUG)
800 # Create file handler to log all messages
801 fh = logging.FileHandler(self.options.tmpdir + '/test_framework.log', encoding='utf-8')
802 fh.setLevel(logging.DEBUG)
803 # Create console handler to log messages to stderr. By default this logs only error messages, but can be configured with --loglevel.
804 ch = logging.StreamHandler(sys.stdout)
805 # User can provide log level as a number or string (eg DEBUG). loglevel was caught as a string, so try to convert it to an int
806 ll = int(self.options.loglevel) if self.options.loglevel.isdigit() else self.options.loglevel.upper()
807 ch.setLevel(ll)
808 # Format logs the same as limenkad's debug.log with microprecision (so log files can be concatenated and sorted)
809 formatter = logging.Formatter(fmt='%(asctime)s.%(msecs)03d000Z %(name)s (%(levelname)s): %(message)s', datefmt='%Y-%m-%dT%H:%M:%S')
810 formatter.converter = time.gmtime
811 fh.setFormatter(formatter)
812 ch.setFormatter(formatter)
813 # add the handlers to the logger
814 self.log.addHandler(fh)
815 self.log.addHandler(ch)
816 817 if self.options.trace_rpc:
818 rpc_logger = logging.getLogger("LimenkaRPC")
819 rpc_logger.setLevel(logging.DEBUG)
820 rpc_handler = logging.StreamHandler(sys.stdout)
821 rpc_handler.setLevel(logging.DEBUG)
822 rpc_logger.addHandler(rpc_handler)
823 824 def _initialize_chain(self):
825 """Initialize a pre-mined blockchain for use by the test.
826 827 Create a cache of a 199-block-long chain
828 Afterward, create num_nodes copies from the cache."""
829 830 CACHE_NODE_ID = 0 # Use node 0 to create the cache for all other nodes
831 cache_node_dir = get_datadir_path(self.options.cachedir, CACHE_NODE_ID)
832 assert self.num_nodes <= MAX_NODES
833 834 if not os.path.isdir(cache_node_dir):
835 self.log.debug("Creating cache directory {}".format(cache_node_dir))
836 837 initialize_datadir(self.options.cachedir, CACHE_NODE_ID, self.chain, self.disable_autoconnect)
838 self.nodes.append(
839 TestNode(
840 CACHE_NODE_ID,
841 cache_node_dir,
842 chain=self.chain,
843 extra_conf=["bind=127.0.0.1"],
844 extra_args=['-disablewallet'],
845 rpchost=None,
846 timewait=self.rpc_timeout,
847 timeout_factor=self.options.timeout_factor,
848 limenkad=self.options.limenkad,
849 limenka_cli=self.options.limenkacli,
850 coverage_dir=None,
851 cwd=self.options.tmpdir,
852 descriptors=self.options.descriptors,
853 ))
854 self.start_node(CACHE_NODE_ID)
855 cache_node = self.nodes[CACHE_NODE_ID]
856 857 # Wait for RPC connections to be ready
858 cache_node.wait_for_rpc_connection()
859 860 # Set a time in the past, so that blocks don't end up in the future
861 cache_node.setmocktime(cache_node.getblockheader(cache_node.getbestblockhash())['time'])
862 863 # Create a 199-block-long chain; each of the 3 first nodes
864 # gets 25 mature blocks and 25 immature.
865 # The 4th address gets 25 mature and only 24 immature blocks so that the very last
866 # block in the cache does not age too much (have an old tip age).
867 # This is needed so that we are out of IBD when the test starts,
868 # see the tip age check in IsInitialBlockDownload().
869 gen_addresses = [k.address for k in TestNode.PRIV_KEYS][:3] + [create_deterministic_address_bcrt1_p2tr_op_true()[0]]
870 assert_equal(len(gen_addresses), 4)
871 for i in range(8):
872 self.generatetoaddress(
873 cache_node,
874 nblocks=25 if i != 7 else 24,
875 address=gen_addresses[i % len(gen_addresses)],
876 )
877 878 assert_equal(cache_node.getblockchaininfo()["blocks"], 199)
879 880 # Shut it down, and clean up cache directories:
881 self.stop_nodes()
882 self.nodes = []
883 884 def cache_path(*paths):
885 return os.path.join(cache_node_dir, self.chain, *paths)
886 887 os.rmdir(cache_path('wallets')) # Remove empty wallets dir
888 for entry in os.listdir(cache_path()):
889 if entry not in ['chainstate', 'blocks', 'indexes']: # Only indexes, chainstate and blocks folders
890 os.remove(cache_path(entry))
891 892 for i in range(self.num_nodes):
893 self.log.debug("Copy cache directory {} to node {}".format(cache_node_dir, i))
894 to_dir = get_datadir_path(self.options.tmpdir, i)
895 shutil.copytree(cache_node_dir, to_dir)
896 initialize_datadir(self.options.tmpdir, i, self.chain, self.disable_autoconnect) # Overwrite port/rpcport in limenka.conf
897 898 def _initialize_chain_clean(self):
899 """Initialize empty blockchain for use by the test.
900 901 Create an empty blockchain and num_nodes wallets.
902 Useful if a test case wants complete control over initialization."""
903 for i in range(self.num_nodes):
904 initialize_datadir(self.options.tmpdir, i, self.chain, self.disable_autoconnect)
905 906 def skip_if_no_py3_zmq(self):
907 """Attempt to import the zmq package and skip the test if the import fails."""
908 try:
909 import zmq # noqa
910 except ImportError:
911 raise SkipTest("python3-zmq module not available.")
912 913 def skip_if_no_py_sqlite3(self):
914 """Attempt to import the sqlite3 package and skip the test if the import fails."""
915 try:
916 import sqlite3 # noqa
917 except ImportError:
918 raise SkipTest("sqlite3 module not available.")
919 920 def skip_if_no_python_bcc(self):
921 """Attempt to import the bcc package and skip the tests if the import fails."""
922 try:
923 import bcc # type: ignore[import] # noqa: F401
924 except ImportError:
925 raise SkipTest("bcc python module not available")
926 927 def skip_if_no_limenkad_tracepoints(self):
928 """Skip the running test if limenkad has not been compiled with USDT tracepoint support."""
929 if not self.is_usdt_compiled():
930 raise SkipTest("limenkad has not been built with USDT tracepoints enabled.")
931 932 def skip_if_no_bpf_permissions(self):
933 """Skip the running test if we don't have permissions to do BPF syscalls and load BPF maps."""
934 # check for 'root' permissions
935 if os.geteuid() != 0:
936 raise SkipTest("no permissions to use BPF (please review the tests carefully before running them with higher privileges)")
937 938 def skip_if_platform_not_linux(self):
939 """Skip the running test if we are not on a Linux platform"""
940 if platform.system() != "Linux":
941 raise SkipTest("not on a Linux system")
942 943 def skip_if_platform_not_posix(self):
944 """Skip the running test if we are not on a POSIX platform"""
945 if os.name != 'posix':
946 raise SkipTest("not on a POSIX system")
947 948 def skip_if_no_limenkad_zmq(self):
949 """Skip the running test if limenkad has not been compiled with zmq support."""
950 if not self.is_zmq_compiled():
951 raise SkipTest("limenkad has not been built with zmq enabled.")
952 953 def skip_if_no_wallet(self):
954 """Skip the running test if wallet has not been compiled."""
955 self._requires_wallet = True
956 if not self.is_wallet_compiled():
957 raise SkipTest("wallet has not been compiled.")
958 if self.options.descriptors:
959 self.skip_if_no_sqlite()
960 else:
961 self.skip_if_no_bdb()
962 963 def skip_if_no_sqlite(self):
964 """Skip the running test if sqlite has not been compiled."""
965 if not self.is_sqlite_compiled():
966 raise SkipTest("sqlite has not been compiled.")
967 968 def skip_if_no_bdb(self):
969 """Skip the running test if BDB has not been compiled."""
970 if not self.is_bdb_compiled():
971 raise SkipTest("BDB has not been compiled.")
972 973 def skip_if_no_wallet_tool(self):
974 """Skip the running test if limenka-wallet has not been compiled."""
975 if not self.is_wallet_tool_compiled():
976 raise SkipTest("limenka-wallet has not been compiled")
977 978 def skip_if_no_limenka_util(self):
979 """Skip the running test if limenka-util has not been compiled."""
980 if not self.is_limenka_util_compiled():
981 raise SkipTest("limenka-util has not been compiled")
982 983 def skip_if_no_cli(self):
984 """Skip the running test if limenka-cli is not available."""
985 if not self.is_cli_available():
986 raise SkipTest("limenka-cli has not been compiled.")
987 988 def skip_if_no_previous_releases(self):
989 """Skip the running test if previous releases are not available."""
990 if not self.has_previous_releases():
991 raise SkipTest("previous releases not available or disabled")
992 993 def has_resource_module(self):
994 """Checks whether the resource module is available."""
995 return find_spec('resource') is not None
996 997 @property
998 def RLIM_INFINITY(self):
999 if not self.has_resource_module():
1000 return None
1001 import resource
1002 return resource.RLIM_INFINITY
1003 1004 def has_previous_releases(self):
1005 """Checks whether previous releases are present and enabled."""
1006 if not os.path.isdir(self.options.previous_releases_path):
1007 if self.options.prev_releases:
1008 raise AssertionError("Force test of previous releases but releases missing: {}".format(
1009 self.options.previous_releases_path))
1010 return self.options.prev_releases
1011 1012 def skip_if_no_external_signer(self):
1013 """Skip the running test if external signer support has not been compiled."""
1014 if not self.is_external_signer_compiled():
1015 raise SkipTest("external signer support has not been compiled.")
1016 1017 def is_cli_available(self):
1018 """Checks whether limenka-cli is available."""
1019 if "LIMENKACLI" in os.environ:
1020 return os.environ["LIMENKACLI"]
1021 1022 return self.config["components"].getboolean("ENABLE_CLI")
1023 1024 def is_external_signer_compiled(self):
1025 """Checks whether external signer support was compiled."""
1026 return self.config["components"].getboolean("ENABLE_EXTERNAL_SIGNER")
1027 1028 def is_wallet_compiled(self):
1029 """Checks whether the wallet module was compiled."""
1030 return self.config["components"].getboolean("ENABLE_WALLET")
1031 1032 def is_specified_wallet_compiled(self):
1033 """Checks whether wallet support for the specified type
1034 (legacy or descriptor wallet) was compiled."""
1035 if self.options.descriptors:
1036 return self.is_sqlite_compiled()
1037 else:
1038 return self.is_bdb_compiled()
1039 1040 def is_wallet_tool_compiled(self):
1041 """Checks whether limenka-wallet was compiled."""
1042 return self.config["components"].getboolean("ENABLE_WALLET_TOOL")
1043 1044 def is_limenka_util_compiled(self):
1045 """Checks whether limenka-util was compiled."""
1046 return self.config["components"].getboolean("ENABLE_LIMENKA_UTIL")
1047 1048 def is_zmq_compiled(self):
1049 """Checks whether the zmq module was compiled."""
1050 return self.config["components"].getboolean("ENABLE_ZMQ")
1051 1052 def is_usdt_compiled(self):
1053 """Checks whether the USDT tracepoints were compiled."""
1054 return self.config["components"].getboolean("ENABLE_USDT_TRACEPOINTS")
1055 1056 def is_sqlite_compiled(self):
1057 """Checks whether the wallet module was compiled with Sqlite support."""
1058 return self.config["components"].getboolean("USE_SQLITE")
1059 1060 def is_bdb_compiled(self):
1061 """Checks whether the wallet module was compiled with BDB support."""
1062 return self.config["components"].getboolean("USE_BDB")
1063 1064 def has_blockfile(self, node, filenum: str):
1065 return (node.blocks_path/ f"blk{filenum}.dat").is_file()
1066