/ test / functional / feature_config_args.py
feature_config_args.py
  1  #!/usr/bin/env python3
  2  # Copyright (c) 2017-2022 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 various command line arguments and configuration file parameters."""
  6  
  7  import os
  8  from pathlib import Path
  9  import platform
 10  import re
 11  import tempfile
 12  import time
 13  
 14  from test_framework.test_framework import BitcoinTestFramework
 15  from test_framework.test_node import ErrorMatch
 16  from test_framework import util
 17  
 18  
 19  class ConfArgsTest(BitcoinTestFramework):
 20      def add_options(self, parser):
 21          self.add_wallet_options(parser)
 22  
 23      def set_test_params(self):
 24          self.setup_clean_chain = True
 25          self.num_nodes = 1
 26          self.supports_cli = False
 27          self.wallet_names = []
 28          self.disable_autoconnect = False
 29  
 30      def test_config_file_parser(self):
 31          self.log.info('Test config file parser')
 32          self.stop_node(0)
 33  
 34          # Check that startup fails if conf= is set in bitcoin.conf or in an included conf file
 35          bad_conf_file_path = self.nodes[0].datadir_path / "bitcoin_bad.conf"
 36          util.write_config(bad_conf_file_path, n=0, chain='', extra_config=f'conf=some.conf\n')
 37          conf_in_config_file_err = 'Error: Error reading configuration file: conf cannot be set in the configuration file; use includeconf= if you want to include additional config files'
 38          self.nodes[0].assert_start_raises_init_error(
 39              extra_args=[f'-conf={bad_conf_file_path}'],
 40              expected_msg=conf_in_config_file_err,
 41          )
 42          inc_conf_file_path = self.nodes[0].datadir_path / 'include.conf'
 43          with open(self.nodes[0].datadir_path / 'bitcoin.conf', 'a', encoding='utf-8') as conf:
 44              conf.write(f'includeconf={inc_conf_file_path}\n')
 45          with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
 46              conf.write('conf=some.conf\n')
 47          self.nodes[0].assert_start_raises_init_error(
 48              expected_msg=conf_in_config_file_err,
 49          )
 50  
 51          self.nodes[0].assert_start_raises_init_error(
 52              expected_msg='Error: Error parsing command line arguments: Invalid parameter -dash_cli=1',
 53              extra_args=['-dash_cli=1'],
 54          )
 55          with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
 56              conf.write('dash_conf=1\n')
 57  
 58          with self.nodes[0].assert_debug_log(expected_msgs=['Ignoring unknown configuration value dash_conf']):
 59              self.start_node(0)
 60          self.stop_node(0)
 61  
 62          with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
 63              conf.write('reindex=1\n')
 64  
 65          with self.nodes[0].assert_debug_log(expected_msgs=['Warning: reindex=1 is set in the configuration file, which will significantly slow down startup. Consider removing or commenting out this option for better performance, unless there is currently a condition which makes rebuilding the indexes necessary']):
 66              self.start_node(0)
 67          self.stop_node(0)
 68  
 69          with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
 70              conf.write('-dash=1\n')
 71          self.nodes[0].assert_start_raises_init_error(expected_msg='Error: Error reading configuration file: parse error on line 1: -dash=1, options in configuration file must be specified without leading -')
 72  
 73          if self.is_wallet_compiled():
 74              with open(inc_conf_file_path, 'w', encoding='utf8') as conf:
 75                  conf.write("wallet=foo\n")
 76              self.nodes[0].assert_start_raises_init_error(expected_msg=f'Error: Config setting for -wallet only applied on {self.chain} network when in [{self.chain}] section.')
 77  
 78          main_conf_file_path = self.nodes[0].datadir_path / "bitcoin_main.conf"
 79          util.write_config(main_conf_file_path, n=0, chain='', extra_config=f'includeconf={inc_conf_file_path}\n')
 80          with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
 81              conf.write('acceptnonstdtxn=1\n')
 82          self.nodes[0].assert_start_raises_init_error(extra_args=[f"-conf={main_conf_file_path}", "-allowignoredconf"], expected_msg='Error: acceptnonstdtxn is not currently supported for main chain')
 83  
 84          with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
 85              conf.write('nono\n')
 86          self.nodes[0].assert_start_raises_init_error(expected_msg='Error: Error reading configuration file: parse error on line 1: nono, if you intended to specify a negated option, use nono=1 instead')
 87  
 88          with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
 89              conf.write('server=1\nrpcuser=someuser\nrpcpassword=some#pass')
 90          self.nodes[0].assert_start_raises_init_error(expected_msg='Error: Error reading configuration file: parse error on line 3, using # in rpcpassword can be ambiguous and should be avoided')
 91  
 92          with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
 93              conf.write('server=1\nrpcuser=someuser\nmain.rpcpassword=some#pass')
 94          self.nodes[0].assert_start_raises_init_error(expected_msg='Error: Error reading configuration file: parse error on line 3, using # in rpcpassword can be ambiguous and should be avoided')
 95  
 96          with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
 97              conf.write('server=1\nrpcuser=someuser\n[main]\nrpcpassword=some#pass')
 98          self.nodes[0].assert_start_raises_init_error(expected_msg='Error: Error reading configuration file: parse error on line 4, using # in rpcpassword can be ambiguous and should be avoided')
 99  
100          inc_conf_file2_path = self.nodes[0].datadir_path / 'include2.conf'
101          with open(self.nodes[0].datadir_path / 'bitcoin.conf', 'a', encoding='utf-8') as conf:
102              conf.write(f'includeconf={inc_conf_file2_path}\n')
103  
104          with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
105              conf.write('testnot.datadir=1\n')
106          with open(inc_conf_file2_path, 'w', encoding='utf-8') as conf:
107              conf.write('[testnet]\n')
108          self.restart_node(0)
109          self.nodes[0].stop_node(expected_stderr=f'Warning: {inc_conf_file_path}:1 Section [testnot] is not recognized.{os.linesep}{inc_conf_file2_path}:1 Section [testnet] is not recognized.')
110  
111          with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
112              conf.write('')  # clear
113          with open(inc_conf_file2_path, 'w', encoding='utf-8') as conf:
114              conf.write('')  # clear
115  
116      def test_config_file_log(self):
117          # Disable this test for windows currently because trying to override
118          # the default datadir through the environment does not seem to work.
119          if platform.system() == "Windows":
120              return
121  
122          self.log.info('Test that correct configuration path is changed when configuration file changes the datadir')
123  
124          # Create a temporary directory that will be treated as the default data
125          # directory by bitcoind.
126          env, default_datadir = util.get_temp_default_datadir(Path(self.options.tmpdir, "test_config_file_log"))
127          default_datadir.mkdir(parents=True)
128  
129          # Write a bitcoin.conf file in the default data directory containing a
130          # datadir= line pointing at the node datadir.
131          node = self.nodes[0]
132          conf_text = node.bitcoinconf.read_text()
133          conf_path = default_datadir / "bitcoin.conf"
134          conf_path.write_text(f"datadir={node.datadir_path}\n{conf_text}")
135  
136          # Drop the node -datadir= argument during this test, because if it is
137          # specified it would take precedence over the datadir setting in the
138          # config file.
139          node_args = node.args
140          node.args = [arg for arg in node.args if not arg.startswith("-datadir=")]
141  
142          # Check that correct configuration file path is actually logged
143          # (conf_path, not node.bitcoinconf)
144          with self.nodes[0].assert_debug_log(expected_msgs=[f"Config file: {conf_path}"]):
145              self.start_node(0, ["-allowignoredconf"], env=env)
146              self.stop_node(0)
147  
148          # Restore node arguments after the test
149          node.args = node_args
150  
151      def test_invalid_command_line_options(self):
152          self.nodes[0].assert_start_raises_init_error(
153              expected_msg='Error: Error parsing command line arguments: Can not set -proxy with no value. Please specify value with -proxy=value.',
154              extra_args=['-proxy'],
155          )
156  
157      def test_log_buffer(self):
158          self.stop_node(0)
159          with self.nodes[0].assert_debug_log(expected_msgs=['Warning: parsed potentially confusing double-negative -connect=0\n']):
160              self.start_node(0, extra_args=['-noconnect=0'])
161  
162      def test_args_log(self):
163          self.stop_node(0)
164          self.log.info('Test config args logging')
165          with self.nodes[0].assert_debug_log(
166                  expected_msgs=[
167                      'Command-line arg: addnode="some.node"',
168                      'Command-line arg: rpcauth=****',
169                      'Command-line arg: rpcpassword=****',
170                      'Command-line arg: rpcuser=****',
171                      'Command-line arg: torpassword=****',
172                      f'Config file arg: {self.chain}="1"',
173                      f'Config file arg: [{self.chain}] server="1"',
174                  ],
175                  unexpected_msgs=[
176                      'alice:f7efda5c189b999524f151318c0c86$d5b51b3beffbc0',
177                      'secret-rpcuser',
178                      'secret-torpassword',
179                      'Command-line arg: rpcbind=****',
180                      'Command-line arg: rpcallowip=****',
181                  ]):
182              self.start_node(0, extra_args=[
183                  '-addnode=some.node',
184                  '-rpcauth=alice:f7efda5c189b999524f151318c0c86$d5b51b3beffbc0',
185                  '-rpcbind=127.1.1.1',
186                  '-rpcbind=127.0.0.1',
187                  "-rpcallowip=127.0.0.1",
188                  '-rpcpassword=',
189                  '-rpcuser=secret-rpcuser',
190                  '-torpassword=secret-torpassword',
191              ])
192  
193      def test_networkactive(self):
194          self.log.info('Test -networkactive option')
195          self.stop_node(0)
196          with self.nodes[0].assert_debug_log(expected_msgs=['SetNetworkActive: true\n']):
197              self.start_node(0)
198  
199          self.stop_node(0)
200          with self.nodes[0].assert_debug_log(expected_msgs=['SetNetworkActive: true\n']):
201              self.start_node(0, extra_args=['-networkactive'])
202  
203          self.stop_node(0)
204          with self.nodes[0].assert_debug_log(expected_msgs=['SetNetworkActive: true\n']):
205              self.start_node(0, extra_args=['-networkactive=1'])
206  
207          self.stop_node(0)
208          with self.nodes[0].assert_debug_log(expected_msgs=['SetNetworkActive: false\n']):
209              self.start_node(0, extra_args=['-networkactive=0'])
210  
211          self.stop_node(0)
212          with self.nodes[0].assert_debug_log(expected_msgs=['SetNetworkActive: false\n']):
213              self.start_node(0, extra_args=['-nonetworkactive'])
214  
215          self.stop_node(0)
216          with self.nodes[0].assert_debug_log(expected_msgs=['SetNetworkActive: false\n']):
217              self.start_node(0, extra_args=['-nonetworkactive=1'])
218  
219      def test_seed_peers(self):
220          self.log.info('Test seed peers')
221          default_data_dir = self.nodes[0].datadir_path
222          peer_dat = default_data_dir / 'peers.dat'
223          # Only regtest has no fixed seeds. To avoid connections to random
224          # nodes, regtest is the only network where it is safe to enable
225          # -fixedseeds in tests
226          util.assert_equal(self.nodes[0].getblockchaininfo()['chain'],'regtest')
227          self.stop_node(0)
228  
229          # No peers.dat exists and -dnsseed=1
230          # We expect the node will use DNS Seeds, but Regtest mode does not have
231          # any valid DNS seeds. So after 60 seconds, the node should fallback to
232          # fixed seeds
233          assert not peer_dat.exists()
234          start = int(time.time())
235          with self.nodes[0].assert_debug_log(
236                  expected_msgs=[
237                      "Loaded 0 addresses from peers.dat",
238                      "0 addresses found from DNS seeds",
239                      "opencon thread start",  # Ensure ThreadOpenConnections::start time is properly set
240                  ],
241                  timeout=10,
242          ):
243              self.start_node(0, extra_args=['-dnsseed=1', '-fixedseeds=1', f'-mocktime={start}'])
244          with self.nodes[0].assert_debug_log(expected_msgs=[
245                  "Adding fixed seeds as 60 seconds have passed and addrman is empty",
246          ]):
247              self.nodes[0].setmocktime(start + 65)
248          self.stop_node(0)
249  
250          # No peers.dat exists and -dnsseed=0
251          # We expect the node will fallback immediately to fixed seeds
252          assert not peer_dat.exists()
253          with self.nodes[0].assert_debug_log(expected_msgs=[
254                  "Loaded 0 addresses from peers.dat",
255                  "DNS seeding disabled",
256                  "Adding fixed seeds as -dnsseed=0 (or IPv4/IPv6 connections are disabled via -onlynet) and neither -addnode nor -seednode are provided\n",
257          ]):
258              self.start_node(0, extra_args=['-dnsseed=0', '-fixedseeds=1'])
259          self.stop_node(0)
260          self.nodes[0].assert_start_raises_init_error(['-dnsseed=1', '-onlynet=i2p', '-i2psam=127.0.0.1:7656'], "Error: Incompatible options: -dnsseed=1 was explicitly specified, but -onlynet forbids connections to IPv4/IPv6")
261  
262          # No peers.dat exists and dns seeds are disabled.
263          # We expect the node will not add fixed seeds when explicitly disabled.
264          assert not peer_dat.exists()
265          with self.nodes[0].assert_debug_log(expected_msgs=[
266                  "Loaded 0 addresses from peers.dat",
267                  "DNS seeding disabled",
268                  "Fixed seeds are disabled",
269          ]):
270              self.start_node(0, extra_args=['-dnsseed=0', '-fixedseeds=0'])
271          self.stop_node(0)
272  
273          # No peers.dat exists and -dnsseed=0, but a -addnode is provided
274          # We expect the node will allow 60 seconds prior to using fixed seeds
275          assert not peer_dat.exists()
276          start = int(time.time())
277          with self.nodes[0].assert_debug_log(
278                  expected_msgs=[
279                      "Loaded 0 addresses from peers.dat",
280                      "DNS seeding disabled",
281                      "opencon thread start",  # Ensure ThreadOpenConnections::start time is properly set
282                  ],
283                  timeout=10,
284          ):
285              self.start_node(0, extra_args=['-dnsseed=0', '-fixedseeds=1', '-addnode=fakenodeaddr', f'-mocktime={start}'])
286          with self.nodes[0].assert_debug_log(expected_msgs=[
287                  "Adding fixed seeds as 60 seconds have passed and addrman is empty",
288          ]):
289              self.nodes[0].setmocktime(start + 65)
290  
291      def test_connect_with_seednode(self):
292          self.log.info('Test -connect with -seednode')
293          seednode_ignored = ['-seednode is ignored when -connect is used\n']
294          dnsseed_ignored = ['-dnsseed is ignored when -connect is used and -proxy is specified\n']
295          addcon_thread_started = ['addcon thread start\n']
296          self.stop_node(0)
297  
298          # When -connect is supplied, expanding addrman via getaddr calls to ADDR_FETCH(-seednode)
299          # nodes is irrelevant and -seednode is ignored.
300          with self.nodes[0].assert_debug_log(expected_msgs=seednode_ignored):
301              self.start_node(0, extra_args=['-connect=fakeaddress1', '-seednode=fakeaddress2'])
302  
303          # With -proxy, an ADDR_FETCH connection is made to a peer that the dns seed resolves to.
304          # ADDR_FETCH connections are not used when -connect is used.
305          with self.nodes[0].assert_debug_log(expected_msgs=dnsseed_ignored):
306              self.restart_node(0, extra_args=['-connect=fakeaddress1', '-dnsseed=1', '-proxy=1.2.3.4'])
307  
308          # If the user did not disable -dnsseed, but it was soft-disabled because they provided -connect,
309          # they shouldn't see a warning about -dnsseed being ignored.
310          with self.nodes[0].assert_debug_log(expected_msgs=addcon_thread_started,
311                  unexpected_msgs=dnsseed_ignored):
312              self.restart_node(0, extra_args=['-connect=fakeaddress1', '-proxy=1.2.3.4'])
313  
314          # We have to supply expected_msgs as it's a required argument
315          # The expected_msg must be something we are confident will be logged after the unexpected_msg
316          # These cases test for -connect being supplied but only to disable it
317          for connect_arg in ['-connect=0', '-noconnect']:
318              with self.nodes[0].assert_debug_log(expected_msgs=addcon_thread_started,
319                      unexpected_msgs=seednode_ignored):
320                  self.restart_node(0, extra_args=[connect_arg, '-seednode=fakeaddress2'])
321  
322      def test_ignored_conf(self):
323          self.log.info('Test error is triggered when the datadir in use contains a bitcoin.conf file that would be ignored '
324                        'because a conflicting -conf file argument is passed.')
325          node = self.nodes[0]
326          with tempfile.NamedTemporaryFile(dir=self.options.tmpdir, mode="wt", delete=False) as temp_conf:
327              temp_conf.write(f"datadir={node.datadir_path}\n")
328          node.assert_start_raises_init_error([f"-conf={temp_conf.name}"], re.escape(
329              f'Error: Data directory "{node.datadir_path}" contains a "bitcoin.conf" file which is ignored, because a '
330              f'different configuration file "{temp_conf.name}" from command line argument "-conf={temp_conf.name}" '
331              f'is being used instead.') + r"[\s\S]*", match=ErrorMatch.FULL_REGEX)
332  
333          # Test that passing a redundant -conf command line argument pointing to
334          # the same bitcoin.conf that would be loaded anyway does not trigger an
335          # error.
336          self.start_node(0, [f'-conf={node.datadir_path}/bitcoin.conf'])
337          self.stop_node(0)
338  
339      def test_ignored_default_conf(self):
340          # Disable this test for windows currently because trying to override
341          # the default datadir through the environment does not seem to work.
342          if platform.system() == "Windows":
343              return
344  
345          self.log.info('Test error is triggered when bitcoin.conf in the default data directory sets another datadir '
346                        'and it contains a different bitcoin.conf file that would be ignored')
347  
348          # Create a temporary directory that will be treated as the default data
349          # directory by bitcoind.
350          env, default_datadir = util.get_temp_default_datadir(Path(self.options.tmpdir, "home"))
351          default_datadir.mkdir(parents=True)
352  
353          # Write a bitcoin.conf file in the default data directory containing a
354          # datadir= line pointing at the node datadir. This will trigger a
355          # startup error because the node datadir contains a different
356          # bitcoin.conf that would be ignored.
357          node = self.nodes[0]
358          (default_datadir / "bitcoin.conf").write_text(f"datadir={node.datadir_path}\n")
359  
360          # Drop the node -datadir= argument during this test, because if it is
361          # specified it would take precedence over the datadir setting in the
362          # config file.
363          node_args = node.args
364          node.args = [arg for arg in node.args if not arg.startswith("-datadir=")]
365          node.assert_start_raises_init_error([], re.escape(
366              f'Error: Data directory "{node.datadir_path}" contains a "bitcoin.conf" file which is ignored, because a '
367              f'different configuration file "{default_datadir}/bitcoin.conf" from data directory "{default_datadir}" '
368              f'is being used instead.') + r"[\s\S]*", env=env, match=ErrorMatch.FULL_REGEX)
369          node.args = node_args
370  
371      def test_acceptstalefeeestimates_arg_support(self):
372          self.log.info("Test -acceptstalefeeestimates option support")
373          conf_file = self.nodes[0].datadir_path / "bitcoin.conf"
374          for chain, chain_name in {("main", ""), ("test", "testnet3"), ("signet", "signet")}:
375              util.write_config(conf_file, n=0, chain=chain_name, extra_config='acceptstalefeeestimates=1\n')
376              self.nodes[0].assert_start_raises_init_error(expected_msg=f'Error: acceptstalefeeestimates is not supported on {chain} chain.')
377          util.write_config(conf_file, n=0, chain="regtest")  # Reset to regtest
378  
379      def run_test(self):
380          self.test_log_buffer()
381          self.test_args_log()
382          self.test_seed_peers()
383          self.test_networkactive()
384          self.test_connect_with_seednode()
385  
386          self.test_config_file_parser()
387          self.test_config_file_log()
388          self.test_invalid_command_line_options()
389          self.test_ignored_conf()
390          self.test_ignored_default_conf()
391          self.test_acceptstalefeeestimates_arg_support()
392  
393          # Remove the -datadir argument so it doesn't override the config file
394          self.nodes[0].args = [arg for arg in self.nodes[0].args if not arg.startswith("-datadir")]
395  
396          default_data_dir = self.nodes[0].datadir_path
397          new_data_dir = default_data_dir / 'newdatadir'
398          new_data_dir_2 = default_data_dir / 'newdatadir2'
399  
400          # Check that using -datadir argument on non-existent directory fails
401          self.nodes[0].datadir_path = new_data_dir
402          self.nodes[0].assert_start_raises_init_error([f'-datadir={new_data_dir}'], f'Error: Specified data directory "{new_data_dir}" does not exist.')
403  
404          # Check that using non-existent datadir in conf file fails
405          conf_file = default_data_dir / "bitcoin.conf"
406  
407          # datadir needs to be set before [chain] section
408          with open(conf_file, encoding='utf8') as f:
409              conf_file_contents = f.read()
410          with open(conf_file, 'w', encoding='utf8') as f:
411              f.write(f"datadir={new_data_dir}\n")
412              f.write(conf_file_contents)
413  
414          self.nodes[0].assert_start_raises_init_error([f'-conf={conf_file}'], f'Error: Error reading configuration file: specified data directory "{new_data_dir}" does not exist.')
415  
416          # Check that an explicitly specified config file that cannot be opened fails
417          none_existent_conf_file = default_data_dir / "none_existent_bitcoin.conf"
418          self.nodes[0].assert_start_raises_init_error(['-conf=' + f'{none_existent_conf_file}'], 'Error: Error reading configuration file: specified config file "' + f'{none_existent_conf_file}' + '" could not be opened.')
419  
420          # Create the directory and ensure the config file now works
421          new_data_dir.mkdir()
422          self.start_node(0, [f'-conf={conf_file}'])
423          self.stop_node(0)
424          assert (new_data_dir / self.chain / 'blocks').exists()
425  
426          # Ensure command line argument overrides datadir in conf
427          new_data_dir_2.mkdir()
428          self.nodes[0].datadir_path = new_data_dir_2
429          self.start_node(0, [f'-datadir={new_data_dir_2}', f'-conf={conf_file}'])
430          assert (new_data_dir_2 / self.chain / 'blocks').exists()
431  
432  
433  if __name__ == '__main__':
434      ConfArgsTest().main()