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()