util.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2014-present The Bitcoin Core developers
   3  # Distributed under the MIT software license, see the accompanying
   4  # file COPYING or http://www.opensource.org/licenses/mit-license.php.
   5  """Helpful routines for regression testing."""
   6  
   7  from base64 import b64encode
   8  from copy import copy
   9  from decimal import Decimal
  10  from subprocess import CalledProcessError
  11  import hashlib
  12  import inspect
  13  import json
  14  import logging
  15  import os
  16  import pathlib
  17  import platform
  18  import random
  19  import re
  20  import shlex
  21  import time
  22  import types
  23  
  24  from .descriptors import descsum_create
  25  from collections.abc import Callable
  26  from typing import Optional, Union
  27  
  28  SATOSHI_PRECISION = Decimal('0.00000001')
  29  
  30  logger = logging.getLogger("TestFramework.utils")
  31  
  32  class JSONRPCException(Exception):
  33      def __init__(self, rpc_error, http_status=None):
  34          self.error = rpc_error
  35          self.http_status = http_status
  36  
  37          # throw KeyError if any required fields are missing
  38          copied_error = copy(rpc_error)
  39          message = copied_error.pop("message")
  40          code = copied_error.pop("code")
  41          extra = f'{copied_error}' if copied_error else ''
  42          super().__init__(f"{message} ({code}) {extra} [http_status={http_status}]")
  43  
  44  # Assert functions
  45  ##################
  46  
  47  
  48  def assert_approx(v, vexp, vspan=0.00001):
  49      """Assert that `v` is within `vspan` of `vexp`"""
  50      if isinstance(v, Decimal) or isinstance(vexp, Decimal):
  51          v=Decimal(v)
  52          vexp=Decimal(vexp)
  53          vspan=Decimal(vspan)
  54      if v < vexp - vspan:
  55          raise AssertionError("%s < [%s..%s]" % (str(v), str(vexp - vspan), str(vexp + vspan)))
  56      if v > vexp + vspan:
  57          raise AssertionError("%s > [%s..%s]" % (str(v), str(vexp - vspan), str(vexp + vspan)))
  58  
  59  
  60  def assert_fee_amount(fee, tx_size, feerate_BTC_kvB):
  61      """Assert the fee is in range."""
  62      assert isinstance(tx_size, int)
  63      target_fee = get_fee(tx_size, feerate_BTC_kvB)
  64      if fee < target_fee:
  65          raise AssertionError("Fee of %s BTC too low! (Should be %s BTC)" % (str(fee), str(target_fee)))
  66      # allow the wallet's estimation to be at most 2 bytes off
  67      high_fee = get_fee(tx_size + 2, feerate_BTC_kvB)
  68      if fee > high_fee:
  69          raise AssertionError("Fee of %s BTC too high! (Should be %s BTC)" % (str(fee), str(target_fee)))
  70  
  71  
  72  def summarise_dict_differences(thing1, thing2):
  73      if not isinstance(thing1, dict) or not isinstance(thing2, dict):
  74          return thing1, thing2
  75      d1, d2 = {}, {}
  76      for k in sorted(thing1.keys()):
  77          if k not in thing2:
  78              d1[k] = thing1[k]
  79          elif thing1[k] != thing2[k]:
  80              d1[k], d2[k] = summarise_dict_differences(thing1[k], thing2[k])
  81      for k in sorted(thing2.keys()):
  82          if k not in thing1:
  83              d2[k] = thing2[k]
  84      return d1, d2
  85  
  86  def assert_equal(thing1, thing2, *args):
  87      if thing1 != thing2 and not args and isinstance(thing1, dict) and isinstance(thing2, dict):
  88          d1,d2 = summarise_dict_differences(thing1, thing2)
  89          if d1 != thing1 or d2 != thing2:
  90              raise AssertionError(f"not({thing1!s} == {thing2!s})\n  in particular not({d1!s} == {d2!s})")
  91          else:
  92              raise AssertionError(f"not({thing1!s} == {thing2!s})")
  93      if thing1 != thing2 or any(thing1 != arg for arg in args):
  94          raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args))
  95  
  96  def assert_not_equal(thing1, thing2, *, error_message=""):
  97      if thing1 == thing2:
  98          raise AssertionError(f"Both values are {thing1}{f', {error_message}' if error_message else ''}")
  99  
 100  
 101  def assert_greater_than(thing1, thing2):
 102      if thing1 <= thing2:
 103          raise AssertionError("%s <= %s" % (str(thing1), str(thing2)))
 104  
 105  
 106  def assert_greater_than_or_equal(thing1, thing2):
 107      if thing1 < thing2:
 108          raise AssertionError("%s < %s" % (str(thing1), str(thing2)))
 109  
 110  
 111  def assert_raises(exc, fun, *args, **kwds):
 112      assert_raises_message(exc, None, fun, *args, **kwds)
 113  
 114  
 115  def assert_raises_message(exc, message, fun, *args, **kwds):
 116      try:
 117          fun(*args, **kwds)
 118      except JSONRPCException:
 119          raise AssertionError("Use assert_raises_rpc_error() to test RPC failures")
 120      except exc as e:
 121          if message is not None and message not in str(e):
 122              raise AssertionError("Expected substring not found in exception:\n"
 123                                   f"substring: '{message}'\nexception: {e!r}.")
 124      except Exception as e:
 125          raise AssertionError("Unexpected exception raised: " + type(e).__name__)
 126      else:
 127          raise AssertionError("No exception raised")
 128  
 129  
 130  def assert_raises_process_error(returncode: int, output: str, fun: Callable, *args, **kwds):
 131      """Execute a process and asserts the process return code and output.
 132  
 133      Calls function `fun` with arguments `args` and `kwds`. Catches a CalledProcessError
 134      and verifies that the return code and output are as expected. Throws AssertionError if
 135      no CalledProcessError was raised or if the return code and output are not as expected.
 136  
 137      Args:
 138          returncode: the process return code.
 139          output: [a substring of] the process output.
 140          fun: the function to call. This should execute a process.
 141          args*: positional arguments for the function.
 142          kwds**: named arguments for the function.
 143      """
 144      try:
 145          fun(*args, **kwds)
 146      except CalledProcessError as e:
 147          if returncode != e.returncode:
 148              raise AssertionError("Unexpected returncode %i" % e.returncode)
 149          if output not in e.output:
 150              raise AssertionError(f"Expected substring not found in: {e.output!r}")
 151      else:
 152          raise AssertionError("No exception raised")
 153  
 154  
 155  def assert_raises_rpc_error(code: Optional[int], message: Optional[str], fun: Callable, *args, **kwds):
 156      """Run an RPC and verify that a specific JSONRPC exception code and message is raised.
 157  
 158      Calls function `fun` with arguments `args` and `kwds`. Catches a JSONRPCException
 159      and verifies that the error code and message are as expected. Throws AssertionError if
 160      no JSONRPCException was raised or if the error code/message are not as expected.
 161  
 162      Args:
 163          code: the error code returned by the RPC call (defined in src/rpc/protocol.h).
 164              Set to None if checking the error code is not required.
 165          message: [a substring of] the error string returned by the RPC call.
 166              Set to None if checking the error string is not required.
 167          fun: the function to call. This should be the name of an RPC.
 168          args*: positional arguments for the function.
 169          kwds**: named arguments for the function.
 170      """
 171      assert try_rpc(code, message, fun, *args, **kwds), "No exception raised"
 172  
 173  
 174  def try_rpc(code, message, fun, *args, **kwds):
 175      """Tries to run an rpc command.
 176  
 177      Test against error code and message if the rpc fails.
 178      Returns whether a JSONRPCException was raised."""
 179      try:
 180          fun(*args, **kwds)
 181      except JSONRPCException as e:
 182          # JSONRPCException was thrown as expected. Check the message and code values are correct.
 183          if (message is not None) and (message not in e.error['message']):
 184              raise AssertionError(
 185                  "Expected substring not found in error message:\nsubstring: '{}'\nerror message: '{}'.".format(
 186                      message, e.error['message']))
 187          if (code is not None) and (code != e.error["code"]):
 188              raise AssertionError("Unexpected JSONRPC error code %i" % e.error["code"])
 189          return True
 190      except Exception as e:
 191          raise AssertionError("Unexpected exception raised: " + type(e).__name__)
 192      else:
 193          return False
 194  
 195  
 196  def assert_is_hex_string(string):
 197      try:
 198          int(string, 16)
 199      except Exception as e:
 200          raise AssertionError("Couldn't interpret %r as hexadecimal; raised: %s" % (string, e))
 201  
 202  
 203  def assert_is_hash_string(string, length=64):
 204      if not isinstance(string, str):
 205          raise AssertionError("Expected a string, got type %r" % type(string))
 206      elif length and len(string) != length:
 207          raise AssertionError("String of length %d expected; got %d" % (length, len(string)))
 208      elif not re.match('[abcdef0-9]+$', string):
 209          raise AssertionError("String %r contains invalid characters for a hash." % string)
 210  
 211  
 212  def assert_array_result(object_array, to_match, expected, should_not_find=False):
 213      """
 214          Pass in array of JSON objects, a dictionary with key/value pairs
 215          to match against, and another dictionary with expected key/value
 216          pairs.
 217          If the should_not_find flag is true, to_match should not be found
 218          in object_array
 219          """
 220      if should_not_find:
 221          assert_equal(expected, {})
 222      num_matched = 0
 223      for item in object_array:
 224          all_match = True
 225          for key, value in to_match.items():
 226              if item[key] != value:
 227                  all_match = False
 228          if not all_match:
 229              continue
 230          elif should_not_find:
 231              num_matched = num_matched + 1
 232          for key, value in expected.items():
 233              if item[key] != value:
 234                  raise AssertionError("%s : expected %s=%s" % (str(item), str(key), str(value)))
 235              num_matched = num_matched + 1
 236      if num_matched == 0 and not should_not_find:
 237          raise AssertionError("No objects matched %s" % (str(to_match)))
 238      if num_matched > 0 and should_not_find:
 239          raise AssertionError("Objects were found %s" % (str(to_match)))
 240  
 241  
 242  # Utility functions
 243  ###################
 244  
 245  
 246  def check_json_precision():
 247      """Make sure json library being used does not lose precision converting BTC values"""
 248      n = Decimal("20000000.00000003")
 249      satoshis = int(json.loads(json.dumps(float(n))) * 1.0e8)
 250      if satoshis != 2000000000000003:
 251          raise RuntimeError("JSON encode/decode loses precision")
 252  
 253  
 254  class Binaries:
 255      """Helper class to provide information about bitcoin binaries
 256  
 257      Attributes:
 258          paths: Object returned from get_binary_paths() containing information
 259              which binaries and command lines to use from environment variables and
 260              the config file.
 261          bin_dir: An optional string containing a directory path to look for
 262              binaries, which takes precedence over the paths above, if specified.
 263              This is used by tests calling binaries from previous releases.
 264      """
 265      def __init__(self, paths, bin_dir, *, use_valgrind=False):
 266          self.paths = paths
 267          self.bin_dir = bin_dir
 268          suppressions_file = pathlib.Path(__file__).resolve().parents[3] / "test" / "sanitizer_suppressions" / "valgrind.supp"
 269          self.valgrind_cmd = [
 270              "valgrind",
 271              f"--suppressions={suppressions_file}",
 272              "--gen-suppressions=all",
 273              "--trace-children=yes",  # Needed for 'bitcoin' wrapper
 274              "--exit-on-first-error=yes",
 275              "--error-exitcode=1",
 276              "--quiet",
 277          ] if use_valgrind else []
 278  
 279      def node_argv(self, **kwargs):
 280          "Return argv array that should be used to invoke bitcoind"
 281          return self._argv("node", self.paths.bitcoind, **kwargs)
 282  
 283      def rpc_argv(self):
 284          "Return argv array that should be used to invoke bitcoin-cli"
 285          # Add -nonamed because "bitcoin rpc" enables -named by default, but bitcoin-cli doesn't
 286          return self._argv("rpc", self.paths.bitcoincli) + ["-nonamed"]
 287  
 288      def bench_argv(self):
 289          "Return argv array that should be used to invoke bench_bitcoin"
 290          return self._argv("bench", self.paths.bitcoin_bench)
 291  
 292      def tx_argv(self):
 293          "Return argv array that should be used to invoke bitcoin-tx"
 294          return self._argv("tx", self.paths.bitcointx)
 295  
 296      def util_argv(self):
 297          "Return argv array that should be used to invoke bitcoin-util"
 298          return self._argv("util", self.paths.bitcoinutil)
 299  
 300      def wallet_argv(self):
 301          "Return argv array that should be used to invoke bitcoin-wallet"
 302          return self._argv("wallet", self.paths.bitcoinwallet)
 303  
 304      def chainstate_argv(self):
 305          "Return argv array that should be used to invoke bitcoin-chainstate"
 306          return self._argv("chainstate", self.paths.bitcoinchainstate)
 307  
 308      def _argv(self, command, bin_path, need_ipc=False):
 309          """Return argv array that should be used to invoke the command.
 310  
 311          It either uses the bitcoin wrapper executable (if BITCOIN_CMD is set or
 312          need_ipc is True), or the direct binary path (bitcoind, etc). When
 313          bin_dir is set (by tests calling binaries from previous releases) it
 314          always uses the direct path.
 315  
 316          The returned args include valgrind, except when bin_dir is set
 317          (previous releases). Also, valgrind will only apply to the bitcoin
 318          wrapper executable directly, not to the commands that `bitcoin` calls.
 319          """
 320          if self.bin_dir is not None:
 321              return [os.path.join(self.bin_dir, os.path.basename(bin_path))]
 322          elif self.paths.bitcoin_cmd is not None or need_ipc:
 323              # If the current test needs IPC functionality, use the bitcoin
 324              # wrapper binary and append -m so it calls multiprocess binaries.
 325              bitcoin_cmd = self.paths.bitcoin_cmd or [self.paths.bitcoin_bin]
 326              return self.valgrind_cmd + bitcoin_cmd + (["-m"] if need_ipc else []) + [command]
 327          else:
 328              return self.valgrind_cmd + [bin_path]
 329  
 330  
 331  def get_binary_paths(config):
 332      """Get paths of all binaries from environment variables or their default values"""
 333  
 334      paths = types.SimpleNamespace()
 335      binaries = {
 336          "bitcoin": "BITCOIN_BIN",
 337          "bitcoind": "BITCOIND",
 338          "bench_bitcoin": "BITCOIN_BENCH",
 339          "bitcoin-cli": "BITCOINCLI",
 340          "bitcoin-util": "BITCOINUTIL",
 341          "bitcoin-tx": "BITCOINTX",
 342          "bitcoin-chainstate": "BITCOINCHAINSTATE",
 343          "bitcoin-wallet": "BITCOINWALLET",
 344      }
 345      # Set paths to bitcoin core binaries allowing overrides with environment
 346      # variables.
 347      for binary, env_variable_name in binaries.items():
 348          default_filename = os.path.join(
 349              config["environment"]["BUILDDIR"],
 350              "bin",
 351              binary + config["environment"]["EXEEXT"],
 352          )
 353          setattr(paths, env_variable_name.lower(), os.getenv(env_variable_name, default=default_filename))
 354      # BITCOIN_CMD environment variable can be specified to invoke bitcoin
 355      # wrapper binary instead of other executables.
 356      paths.bitcoin_cmd = shlex.split(os.getenv("BITCOIN_CMD", "")) or None
 357      return paths
 358  
 359  
 360  def export_env_build_path(config):
 361      os.environ["PATH"] = os.pathsep.join([
 362          os.path.join(config["environment"]["BUILDDIR"], "bin"),
 363          os.environ["PATH"],
 364      ])
 365  
 366  
 367  def count_bytes(hex_string):
 368      return len(bytearray.fromhex(hex_string))
 369  
 370  
 371  def str_to_b64str(string):
 372      return b64encode(string.encode('utf-8')).decode('ascii')
 373  
 374  
 375  def ceildiv(a, b):
 376      """
 377      Divide 2 ints and round up to next int rather than round down
 378      Implementation requires python integers, which have a // operator that does floor division.
 379      Other types like decimal.Decimal whose // operator truncates towards 0 will not work.
 380      """
 381      assert isinstance(a, int)
 382      assert isinstance(b, int)
 383      return -(-a // b)
 384  
 385  
 386  def random_bitflip(data):
 387      data = list(data)
 388      data[random.randrange(len(data))] ^= (1 << (random.randrange(8)))
 389      return bytes(data)
 390  
 391  
 392  def get_fee(tx_size, feerate_btc_kvb):
 393      """Calculate the fee in BTC given a feerate is BTC/kvB. Reflects CFeeRate::GetFee"""
 394      feerate_sat_kvb = int(feerate_btc_kvb * Decimal(1e8)) # Fee in sat/kvb as an int to avoid float precision errors
 395      target_fee_sat = ceildiv(feerate_sat_kvb * tx_size, 1000) # Round calculated fee up to nearest sat
 396      return target_fee_sat / Decimal(1e8) # Return result in  BTC
 397  
 398  
 399  def satoshi_round(amount: Union[int, float, str], *, rounding: str) -> Decimal:
 400      """Rounds a Decimal amount to the nearest satoshi using the specified rounding mode."""
 401      return Decimal(amount).quantize(SATOSHI_PRECISION, rounding=rounding)
 402  
 403  
 404  def ensure_for(*, duration, f, check_interval=0.2):
 405      """Check if the predicate keeps returning True for duration.
 406  
 407      check_interval can be used to configure the wait time between checks.
 408      Setting check_interval to 0 will allow to have two checks: one in the
 409      beginning and one after duration.
 410      """
 411      # If check_interval is 0 or negative or larger than duration, we fall back
 412      # to checking once in the beginning and once at the end of duration
 413      if check_interval <= 0 or check_interval > duration:
 414          check_interval = duration
 415      time_end = time.time() + duration
 416      predicate_source = "''''\n" + inspect.getsource(f) + "'''"
 417      while True:
 418          if not f():
 419              raise AssertionError(f"Predicate {predicate_source} became false within {duration} seconds")
 420          if time.time() > time_end:
 421              return
 422          time.sleep(check_interval)
 423  
 424  
 425  def wait_until_helper_internal(predicate, *, timeout=60, lock=None, timeout_factor=1.0, check_interval=0.05):
 426      """Sleep until the predicate resolves to be True.
 427  
 428      Warning: Note that this method is not recommended to be used in tests as it is
 429      not aware of the context of the test framework. Using the `wait_until()` members
 430      from `BitcoinTestFramework` or `P2PInterface` class ensures the timeout is
 431      properly scaled. Furthermore, `wait_until()` from `P2PInterface` class in
 432      `p2p.py` has a preset lock.
 433      """
 434      timeout = timeout * timeout_factor
 435      time_end = time.time() + timeout
 436  
 437      while time.time() < time_end:
 438          if lock:
 439              with lock:
 440                  if predicate():
 441                      return
 442          else:
 443              if predicate():
 444                  return
 445          time.sleep(check_interval)
 446  
 447      # Print the cause of the timeout
 448      predicate_source = "''''\n" + inspect.getsource(predicate) + "'''"
 449      logger.error("wait_until() failed. Predicate: {}".format(predicate_source))
 450      raise AssertionError("Predicate {} not true after {} seconds".format(predicate_source, timeout))
 451  
 452  
 453  def bpf_cflags():
 454      return [
 455          "-Wno-error=implicit-function-declaration",
 456          "-Wno-duplicate-decl-specifier",  # https://github.com/bitcoin/bitcoin/issues/32322
 457      ]
 458  
 459  
 460  def sha256sum_file(filename):
 461      h = hashlib.sha256()
 462      with open(filename, 'rb') as f:
 463          d = f.read(4096)
 464          while len(d) > 0:
 465              h.update(d)
 466              d = f.read(4096)
 467      return h.digest()
 468  
 469  
 470  def util_xor(data, key, *, offset):
 471      data = bytearray(data)
 472      for i in range(len(data)):
 473          data[i] ^= key[(i + offset) % len(key)]
 474      return bytes(data)
 475  
 476  
 477  # RPC/P2P connection constants and functions
 478  ############################################
 479  
 480  # The maximum number of nodes a single test can spawn
 481  MAX_NODES = 12
 482  # Don't assign p2p, rpc or tor ports lower than this
 483  PORT_MIN = int(os.getenv('TEST_RUNNER_PORT_MIN', default=11000))
 484  # The number of ports to "reserve" for p2p, rpc and tor, each
 485  PORT_RANGE = 5000
 486  
 487  
 488  class PortSeed:
 489      # Must be initialized with a unique integer for each process
 490      n = None
 491  
 492  def p2p_port(n):
 493      assert n <= MAX_NODES
 494      return PORT_MIN + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES)
 495  
 496  
 497  def rpc_port(n):
 498      return p2p_port(n) + PORT_RANGE
 499  
 500  
 501  def tor_port(n):
 502      return p2p_port(n) + PORT_RANGE * 2
 503  
 504  
 505  # Node functions
 506  ################
 507  
 508  
 509  def initialize_datadir(dirname, n, chain, disable_autoconnect=True):
 510      datadir = get_datadir_path(dirname, n)
 511      if not os.path.isdir(datadir):
 512          os.makedirs(datadir)
 513      write_config(os.path.join(datadir, "bitcoin.conf"), n=n, chain=chain, disable_autoconnect=disable_autoconnect)
 514      os.makedirs(os.path.join(datadir, 'stderr'), exist_ok=True)
 515      os.makedirs(os.path.join(datadir, 'stdout'), exist_ok=True)
 516      return datadir
 517  
 518  
 519  def write_config(config_path, *, n, chain, extra_config="", disable_autoconnect=True):
 520      # Translate chain subdirectory name to config name
 521      if chain == 'testnet3':
 522          chain_name_conf_arg = 'testnet'
 523          chain_name_conf_section = 'test'
 524      else:
 525          chain_name_conf_arg = chain
 526          chain_name_conf_section = chain
 527      with open(config_path, 'w') as f:
 528          if chain_name_conf_arg:
 529              f.write("{}=1\n".format(chain_name_conf_arg))
 530          if chain_name_conf_section:
 531              f.write("[{}]\n".format(chain_name_conf_section))
 532          f.write("port=" + str(p2p_port(n)) + "\n")
 533          f.write("rpcport=" + str(rpc_port(n)) + "\n")
 534          # Disable server-side timeouts to avoid intermittent issues
 535          f.write("rpcservertimeout=99000\n")
 536          f.write("rpcdoccheck=1\n")
 537          f.write("rpcthreads=2\n")
 538          f.write("fallbackfee=0.0002\n")
 539          f.write("server=1\n")
 540          f.write("keypool=1\n")
 541          f.write("discover=0\n")
 542          f.write("dnsseed=0\n")
 543          f.write("fixedseeds=0\n")
 544          f.write("listenonion=0\n")
 545          # Increase peertimeout to avoid disconnects while using mocktime.
 546          # peertimeout is measured in mock time, so setting it large enough to
 547          # cover any duration in mock time is sufficient. It can be overridden
 548          # in tests.
 549          f.write("peertimeout=999999999\n")
 550          f.write("printtoconsole=0\n")
 551          f.write("natpmp=0\n") # Avoid non-loopback network traffic during tests.
 552          f.write("shrinkdebugfile=0\n")
 553          # To improve SQLite wallet performance so that the tests don't timeout, use -unsafesqlitesync
 554          f.write("unsafesqlitesync=1\n")
 555          if disable_autoconnect:
 556              f.write("connect=0\n")
 557          # Limit max connections to mitigate test failures on some systems caused by the warning:
 558          # "Warning: Reducing -maxconnections from <...> to <...> due to system limitations".
 559          # The value is calculated as follows:
 560          #  available_fds = 256          // Same as FD_SETSIZE on NetBSD.
 561          #  MIN_CORE_FDS = 151           // Number of file descriptors required for core functionality.
 562          #  MAX_ADDNODE_CONNECTIONS = 8  // Maximum number of -addnode outgoing nodes.
 563          #  nBind == 3                   // Maximum number of bound interfaces used in a test.
 564          #
 565          #  min_required_fds = MIN_CORE_FDS + MAX_ADDNODE_CONNECTIONS + nBind = 151 + 8 + 3 = 162;
 566          #  nMaxConnections = available_fds - min_required_fds = 256 - 161 = 94;
 567          f.write("maxconnections=94\n")
 568          f.write("par=" + str(min(2, os.cpu_count())) + "\n")
 569          # Use a single prevoutfetch worker thread to keep per-node resource usage low.
 570          f.write("prevoutfetchthreads=1\n")
 571          f.write(extra_config)
 572  
 573  
 574  def get_datadir_path(dirname, n):
 575      return pathlib.Path(dirname) / f"node{n}"
 576  
 577  
 578  def get_temp_default_datadir(temp_dir: pathlib.Path) -> tuple[dict, pathlib.Path]:
 579      """Return os-specific environment variables that can be set to make the
 580      GetDefaultDataDir() function return a datadir path under the provided
 581      temp_dir, as well as the complete path it would return."""
 582      if platform.system() == "Windows":
 583          env = dict(APPDATA=str(temp_dir))
 584          datadir = temp_dir / "Bitcoin"
 585      else:
 586          env = dict(HOME=str(temp_dir))
 587          if platform.system() == "Darwin":
 588              datadir = temp_dir / "Library/Application Support/Bitcoin"
 589          else:
 590              datadir = temp_dir / ".bitcoin"
 591      return env, datadir
 592  
 593  
 594  def append_config(datadir, options):
 595      with open(os.path.join(datadir, "bitcoin.conf"), 'a') as f:
 596          for option in options:
 597              f.write(option + "\n")
 598  
 599  
 600  def get_auth_cookie(datadir, chain):
 601      user = None
 602      password = None
 603      if os.path.isfile(os.path.join(datadir, "bitcoin.conf")):
 604          with open(os.path.join(datadir, "bitcoin.conf"), 'r') as f:
 605              for line in f:
 606                  if line.startswith("rpcuser="):
 607                      assert user is None  # Ensure that there is only one rpcuser line
 608                      user = line.split("=")[1].strip("\n")
 609                  if line.startswith("rpcpassword="):
 610                      assert password is None  # Ensure that there is only one rpcpassword line
 611                      password = line.split("=")[1].strip("\n")
 612      try:
 613          with open(os.path.join(datadir, chain, ".cookie"), 'r') as f:
 614              userpass = f.read()
 615              split_userpass = userpass.split(':')
 616              user = split_userpass[0]
 617              password = split_userpass[1]
 618      except OSError:
 619          pass
 620      if user is None or password is None:
 621          raise ValueError("No RPC credentials")
 622      return user, password
 623  
 624  
 625  # If a cookie file exists in the given datadir, delete it.
 626  def delete_cookie_file(datadir, chain):
 627      if os.path.isfile(os.path.join(datadir, chain, ".cookie")):
 628          logger.debug("Deleting leftover cookie file")
 629          os.remove(os.path.join(datadir, chain, ".cookie"))
 630  
 631  
 632  def softfork_active(node, key):
 633      """Return whether a softfork is active."""
 634      return node.getdeploymentinfo()['deployments'][key]['active']
 635  
 636  
 637  def set_node_times(nodes, t):
 638      for node in nodes:
 639          node.setmocktime(t)
 640  
 641  
 642  def check_node_connections(*, node, num_in, num_out):
 643      info = node.getnetworkinfo()
 644      assert_equal(info["connections_in"], num_in)
 645      assert_equal(info["connections_out"], num_out)
 646  
 647  
 648  # Transaction/Block functions
 649  #############################
 650  
 651  
 652  # Create large OP_RETURN txouts that can be appended to a transaction
 653  # to make it large (helper for constructing large transactions). The
 654  # total serialized size of the txouts is about 66k vbytes.
 655  def gen_return_txouts():
 656      from .messages import CTxOut
 657      from .script import CScript, OP_RETURN
 658      txouts = [CTxOut(nValue=0, scriptPubKey=CScript([OP_RETURN, b'\x01'*67437]))]
 659      assert_equal(sum([len(txout.serialize()) for txout in txouts]), 67456)
 660      return txouts
 661  
 662  
 663  # Create a spend of each passed-in utxo, splicing in "txouts" to each raw
 664  # transaction to make it large.  See gen_return_txouts() above.
 665  def create_lots_of_big_transactions(mini_wallet, node, fee, tx_batch_size, txouts, utxos=None):
 666      txids = []
 667      use_internal_utxos = utxos is None
 668      for _ in range(tx_batch_size):
 669          tx = mini_wallet.create_self_transfer(
 670              utxo_to_spend=None if use_internal_utxos else utxos.pop(),
 671              fee=fee,
 672          )["tx"]
 673          tx.vout.extend(txouts)
 674          res = node.testmempoolaccept([tx.serialize().hex()])[0]
 675          assert_equal(res['fees']['base'], fee)
 676          txids.append(node.sendrawtransaction(tx.serialize().hex()))
 677      return txids
 678  
 679  
 680  def mine_large_block(test_framework, mini_wallet, node):
 681      # generate a 66k transaction,
 682      # and 14 of them is close to the 1MB block limit
 683      txouts = gen_return_txouts()
 684      fee = 100 * node.getnetworkinfo()["relayfee"]
 685      create_lots_of_big_transactions(mini_wallet, node, fee, 14, txouts)
 686      test_framework.generate(node, 1)
 687  
 688  
 689  def find_vout_for_address(node, txid, addr):
 690      """
 691      Locate the vout index of the given transaction sending to the
 692      given address. Raises runtime error exception if not found.
 693      """
 694      tx = node.getrawtransaction(txid, True)
 695      for i in range(len(tx["vout"])):
 696          if addr == tx["vout"][i]["scriptPubKey"]["address"]:
 697              return i
 698      raise RuntimeError("Vout not found for address: txid=%s, addr=%s" % (txid, addr))
 699  
 700  
 701  def dumb_sync_blocks(*, src, dst, height=None):
 702      """Sync blocks between `src` and `dst` nodes via RPC submitblock up to height."""
 703      height = height or src.getblockcount()
 704      for i in range(dst.getblockcount() + 1, height + 1):
 705          block_hash = src.getblockhash(i)
 706          block = src.getblock(blockhash=block_hash, verbosity=0)
 707          dst.submitblock(block)
 708      assert_equal(dst.getblockcount(), height)
 709  
 710  
 711  def sync_txindex(test_framework, node):
 712      test_framework.log.debug("Waiting for node txindex to sync")
 713      sync_start = int(time.time())
 714      test_framework.wait_until(lambda: node.getindexinfo("txindex")["txindex"]["synced"])
 715      test_framework.log.debug(f"Synced in {time.time() - sync_start} seconds")
 716  
 717  def wallet_importprivkey(wallet_rpc, privkey, timestamp, *, label=""):
 718      desc = descsum_create("combo(" + privkey + ")")
 719      req = [{
 720          "desc": desc,
 721          "timestamp": timestamp,
 722          "label": label,
 723      }]
 724      import_res = wallet_rpc.importdescriptors(req)
 725      assert_equal(import_res[0]["success"], True)
 726  
 727  def is_dir_writable(dir_path: pathlib.Path) -> bool:
 728      """Return True if we can create a file in the directory, False otherwise"""
 729      try:
 730          tmp = dir_path / f".tmp_{random.randrange(1 << 32)}"
 731          tmp.touch()
 732          tmp.unlink()
 733          return True
 734      except OSError:
 735          return False
 736