test_framework.py raw

   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