/ test / functional / feature_asmap.py
feature_asmap.py
  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()