feature_asmap.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2020-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  """Test asmap config argument for ASN-based IP bucketing.
   6  
   7  Verify node behaviour and debug log when launching bitcoind with different
   8  `-asmap` and `-noasmap` arg values, including absolute and relative paths, and
   9  with missing and unparseable files.
  10  
  11  The tests are order-independent.
  12  
  13  """
  14  import hashlib
  15  import os
  16  import shutil
  17  
  18  from test_framework.test_framework import BitcoinTestFramework
  19  from test_framework.util import (
  20      assert_equal,
  21      assert_raises_rpc_error,
  22  )
  23  
  24  ASMAP = 'src/test/data/asmap.raw' # path to unit test skeleton asmap
  25  VERSION = 'bafc9da308f45179443bd1d22325400ac9104f741522d003e3fac86700f68895'
  26  
  27  def expected_messages(filename):
  28      return [f'Opened asmap file "{filename}" (59 bytes) from disk',
  29              f'Using asmap version {VERSION} for IP bucketing']
  30  
  31  class AsmapTest(BitcoinTestFramework):
  32      def set_test_params(self):
  33          self.num_nodes = 1
  34          # Do addrman checks on all operations and use deterministic addrman
  35          self.extra_args = [["-checkaddrman=1", "-test=addrman"]]
  36  
  37      def fill_addrman(self, node_id):
  38          """Add 2 tried addresses to the addrman, followed by 2 new addresses."""
  39          for addr, tried in [[0, True], [1, True], [2, False], [3, False]]:
  40              self.nodes[node_id].addpeeraddress(address=f"101.{addr}.0.0", tried=tried, port=8333)
  41  
  42      def test_without_asmap_arg(self):
  43          self.log.info('Test bitcoind with no -asmap arg passed')
  44          self.stop_node(0)
  45          with self.node.assert_debug_log(['Using /16 prefix for IP bucketing']):
  46              self.start_node(0)
  47  
  48      def test_noasmap_arg(self):
  49          self.log.info('Test bitcoind with -noasmap arg passed')
  50          self.stop_node(0)
  51          with self.node.assert_debug_log(['Using /16 prefix for IP bucketing']):
  52              self.start_node(0, ["-noasmap"])
  53  
  54      def test_asmap_with_absolute_path(self):
  55          self.log.info('Test bitcoind -asmap=<absolute path>')
  56          self.stop_node(0)
  57          filename = os.path.join(self.datadir, 'my-map-file.map')
  58          shutil.copyfile(self.asmap_raw, filename)
  59          with self.node.assert_debug_log(expected_messages(filename)):
  60              self.start_node(0, [f'-asmap={filename}'])
  61          os.remove(filename)
  62  
  63      def test_asmap_with_relative_path(self):
  64          self.log.info('Test bitcoind -asmap=<relative path>')
  65          self.stop_node(0)
  66          name = 'ASN_map'
  67          filename = os.path.join(self.datadir, name)
  68          shutil.copyfile(self.asmap_raw, filename)
  69          with self.node.assert_debug_log(expected_messages(filename)):
  70              self.start_node(0, [f'-asmap={name}'])
  71          os.remove(filename)
  72  
  73      def test_embedded_asmap(self):
  74          if self.is_embedded_asmap_compiled():
  75              self.log.info('Test bitcoind -asmap (using embedded map data)')
  76              for arg in ['-asmap', '-asmap=1']:
  77                  self.stop_node(0)
  78                  with self.node.assert_debug_log(["Opened asmap data", "from embedded byte array"]):
  79                      self.start_node(0, [arg])
  80          else:
  81              self.log.info('Test bitcoind -asmap (compiled without embedded map data)')
  82              for arg in ['-asmap', '-asmap=1']:
  83                  self.stop_node(0)
  84                  msg = "Error: Embedded asmap data not available"
  85                  self.node.assert_start_raises_init_error(extra_args=[arg], expected_msg=msg)
  86  
  87      def test_asmap_interaction_with_addrman_containing_entries(self):
  88          self.log.info("Test bitcoind -asmap restart with addrman containing new and tried entries")
  89          self.stop_node(0)
  90          self.start_node(0, [f"-asmap={self.asmap_raw}", "-checkaddrman=1", "-test=addrman"])
  91          self.fill_addrman(node_id=0)
  92          self.restart_node(0, [f"-asmap={self.asmap_raw}", "-checkaddrman=1", "-test=addrman"])
  93          with self.node.assert_debug_log(
  94              expected_msgs=[
  95                  "CheckAddrman: new 2, tried 2, total 4 started",
  96                  "CheckAddrman: completed",
  97              ]
  98          ):
  99              self.node.getnodeaddresses()  # getnodeaddresses re-runs the addrman checks
 100  
 101      def test_asmap_with_missing_file(self):
 102          self.log.info('Test bitcoind -asmap with missing map file')
 103          self.stop_node(0)
 104          msg = f"Error: Could not find asmap file \"{self.datadir}{os.sep}missing\""
 105          self.node.assert_start_raises_init_error(extra_args=['-asmap=missing'], expected_msg=msg)
 106  
 107      def test_empty_asmap(self):
 108          self.log.info('Test bitcoind -asmap with empty map file')
 109          self.stop_node(0)
 110          empty_asmap = os.path.join(self.datadir, "ip_asn.map")
 111          with open(empty_asmap, "w") as f:
 112              f.write("")
 113          msg = f"Error: Could not parse asmap file \"{empty_asmap}\""
 114          self.node.assert_start_raises_init_error(extra_args=[f'-asmap={empty_asmap}'], expected_msg=msg)
 115          os.remove(empty_asmap)
 116  
 117      def test_asmap_health_check(self):
 118          self.log.info('Test bitcoind -asmap logs ASMap Health Check with basic stats')
 119          msg = "ASMap Health Check: 4 clearnet peers are mapped to 3 ASNs with 0 peers being unmapped"
 120          with self.node.assert_debug_log(expected_msgs=[msg]):
 121              self.start_node(0, extra_args=[f'-asmap={self.asmap_raw}'])
 122          raw_addrman = self.node.getrawaddrman()
 123          asns = []
 124          for _, entries in raw_addrman.items():
 125              for _, entry in entries.items():
 126                  asn = entry['mapped_as']
 127                  if asn not in asns:
 128                      asns.append(asn)
 129          assert_equal(len(asns), 3)
 130  
 131      def test_export_embedded_asmap(self):
 132          self.log.info('Test exportasmap RPC')
 133          export_path = os.path.join(self.datadir, "asmap.dat")
 134  
 135          if not self.is_embedded_asmap_compiled():
 136              assert_raises_rpc_error(-1, "No embedded ASMap data available", self.node.exportasmap, export_path)
 137              return
 138  
 139          # Relative paths are resolved against the datadir.
 140          result = self.node.exportasmap("asmap.dat")
 141          assert_equal(result["path"], export_path)
 142  
 143          with open(export_path, 'rb') as f:
 144              data = f.read()
 145          assert_equal(result["bytes_written"], len(data))
 146  
 147          # Added in https://github.com/bitcoin/bitcoin/pull/34696
 148          expected_hash = "478d61986c59365cf86cd244485bbbe76a9ca0c630864717286dd19949879074"
 149          assert_equal(hashlib.sha256(data).hexdigest(), expected_hash)
 150          assert_equal(result["file_hash"], expected_hash)
 151  
 152          os.remove(export_path)
 153  
 154      def run_test(self):
 155          self.node = self.nodes[0]
 156          self.datadir = self.node.chain_path
 157          base_dir = self.config["environment"]["SRCDIR"]
 158          self.asmap_raw = os.path.join(base_dir, ASMAP)
 159  
 160          self.test_without_asmap_arg()
 161          self.test_noasmap_arg()
 162          self.test_asmap_with_absolute_path()
 163          self.test_asmap_with_relative_path()
 164          self.test_embedded_asmap()
 165          self.test_asmap_interaction_with_addrman_containing_entries()
 166          self.test_asmap_with_missing_file()
 167          self.test_empty_asmap()
 168          self.test_asmap_health_check()
 169          self.test_export_embedded_asmap()
 170  
 171  
 172  if __name__ == '__main__':
 173      AsmapTest(__file__).main()
 174