Skip to content

Commit 3746f00

Browse files
committed
init: Error if ignored bitcoin.conf file is found
Show an error on startup if a bitcoin datadir that is being used contains a `bitcoin.conf` file that is ignored. There are two cases where this could happen: - One case reported in bitcoin/bitcoin#27246 (comment) happens when a bitcoin.conf file in the default datadir (e.g. $HOME/.bitcoin/bitcoin.conf) has a "datadir=/path" line that sets different datadir containing a second bitcoin.conf file. Currently the second bitcoin.conf file is ignored with no warning. - Another way this could happen is if a -conf= command line argument points to a configuration file with a "datadir=/path" line and that specified path contains a bitcoin.conf file, which is currently ignored. This change only adds an error message and doesn't change anything about way settings are applied. It also doesn't trigger errors if there are redundant -datadir or -conf settings pointing at the same configuration file, only if they are pointing at different files and one file is being ignored.
1 parent 398c371 commit 3746f00

File tree

6 files changed

+124
-3
lines changed

6 files changed

+124
-3
lines changed

doc/release-notes-27302.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Configuration
2+
---
3+
4+
- `bitcoind` and `bitcoin-qt` will now raise an error on startup if a datadir that is being used contains a bitcoin.conf file that will be ignored, which can happen when a datadir= line is used in a bitcoin.conf file. The error message is just a diagnostic intended to prevent accidental misconfiguration, and it can be disabled to restore the previous behavior of using the datadir while ignoring the bitcoin.conf contained in it.

src/common/init.cpp

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <chainparams.h>
66
#include <common/args.h>
77
#include <common/init.h>
8+
#include <logging.h>
89
#include <tinyformat.h>
910
#include <util/fs.h>
1011
#include <util/translation.h>
@@ -20,6 +21,19 @@ std::optional<ConfigError> InitConfig(ArgsManager& args, SettingsAbortFn setting
2021
if (!CheckDataDirOption(args)) {
2122
return ConfigError{ConfigStatus::FAILED, strprintf(_("Specified data directory \"%s\" does not exist."), args.GetArg("-datadir", ""))};
2223
}
24+
25+
// Record original datadir and config paths before parsing the config
26+
// file. It is possible for the config file to contain a datadir= line
27+
// that changes the datadir path after it is parsed. This is useful for
28+
// CLI tools to let them use a different data storage location without
29+
// needing to pass it every time on the command line. (It is not
30+
// possible for the config file to cause another configuration to be
31+
// used, though. Specifying a conf= option in the config file causes a
32+
// parse error, and specifying a datadir= location containing another
33+
// bitcoin.conf file just ignores the other file.)
34+
const fs::path orig_datadir_path{args.GetDataDirBase()};
35+
const fs::path orig_config_path = args.GetConfigFilePath();
36+
2337
std::string error;
2438
if (!args.ReadConfigFiles(error, true)) {
2539
return ConfigError{ConfigStatus::FAILED, strprintf(_("Error reading configuration file: %s"), error)};
@@ -48,6 +62,32 @@ std::optional<ConfigError> InitConfig(ArgsManager& args, SettingsAbortFn setting
4862
fs::create_directories(net_path / "wallets");
4963
}
5064

65+
// Show an error or warning if there is a bitcoin.conf file in the
66+
// datadir that is being ignored.
67+
const fs::path base_config_path = base_path / BITCOIN_CONF_FILENAME;
68+
if (fs::exists(base_config_path) && !fs::equivalent(orig_config_path, base_config_path)) {
69+
const std::string cli_config_path = args.GetArg("-conf", "");
70+
const std::string config_source = cli_config_path.empty()
71+
? strprintf("data directory %s", fs::quoted(fs::PathToString(orig_datadir_path)))
72+
: strprintf("command line argument %s", fs::quoted("-conf=" + cli_config_path));
73+
const std::string error = strprintf(
74+
"Data directory %1$s contains a %2$s file which is ignored, because a different configuration file "
75+
"%3$s from %4$s is being used instead. Possible ways to address this would be to:\n"
76+
"- Delete or rename the %2$s file in data directory %1$s.\n"
77+
"- Change datadir= or conf= options to specify one configuration file, not two, and use "
78+
"includeconf= to include any other configuration files.\n"
79+
"- Set allowignoredconf=1 option to treat this condition as a warning, not an error.",
80+
fs::quoted(fs::PathToString(base_path)),
81+
fs::quoted(BITCOIN_CONF_FILENAME),
82+
fs::quoted(fs::PathToString(orig_config_path)),
83+
config_source);
84+
if (args.GetBoolArg("-allowignoredconf", false)) {
85+
LogPrintf("Warning: %s\n", error);
86+
} else {
87+
return ConfigError{ConfigStatus::FAILED, Untranslated(error)};
88+
}
89+
}
90+
5191
// Create settings.json if -nosettings was not specified.
5292
if (args.GetSettingsPath()) {
5393
std::vector<std::string> details;

src/init.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,7 @@ void SetupServerArgs(ArgsManager& argsman)
441441
argsman.AddArg("-dbbatchsize", strprintf("Maximum database write batch size in bytes (default: %u)", nDefaultDbBatchSize), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::OPTIONS);
442442
argsman.AddArg("-dbcache=<n>", strprintf("Maximum database cache size <n> MiB (%d to %d, default: %d). In addition, unused mempool memory is shared for this cache (see -maxmempool).", nMinDbCache, nMaxDbCache, nDefaultDbCache), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
443443
argsman.AddArg("-includeconf=<file>", "Specify additional configuration file, relative to the -datadir path (only useable from configuration file, not command line)", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
444+
argsman.AddArg("-allowignoredconf", strprintf("For backwards compatibility, treat an unused %s file in the datadir as a warning, not an error.", BITCOIN_CONF_FILENAME), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
444445
argsman.AddArg("-loadblock=<file>", "Imports blocks from external file on startup", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
445446
argsman.AddArg("-maxmempool=<n>", strprintf("Keep the transaction memory pool below <n> megabytes (default: %u)", DEFAULT_MAX_MEMPOOL_SIZE_MB), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
446447
argsman.AddArg("-maxorphantx=<n>", strprintf("Keep at most <n> unconnectable transactions in memory (default: %u)", DEFAULT_MAX_ORPHAN_TRANSACTIONS), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);

test/functional/feature_config_args.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@
55
"""Test various command line arguments and configuration file parameters."""
66

77
import os
8+
import pathlib
9+
import re
10+
import sys
11+
import tempfile
812
import time
913

1014
from test_framework.test_framework import BitcoinTestFramework
15+
from test_framework.test_node import ErrorMatch
1116
from test_framework import util
1217

1318

@@ -74,7 +79,7 @@ def test_config_file_parser(self):
7479
util.write_config(main_conf_file_path, n=0, chain='', extra_config=f'includeconf={inc_conf_file_path}\n')
7580
with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
7681
conf.write('acceptnonstdtxn=1\n')
77-
self.nodes[0].assert_start_raises_init_error(extra_args=[f"-conf={main_conf_file_path}"], expected_msg='Error: acceptnonstdtxn is not currently supported for main chain')
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')
7883

7984
with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
8085
conf.write('nono\n')
@@ -282,6 +287,55 @@ def test_connect_with_seednode(self):
282287
unexpected_msgs=seednode_ignored):
283288
self.restart_node(0, extra_args=[connect_arg, '-seednode=fakeaddress2'])
284289

290+
def test_ignored_conf(self):
291+
self.log.info('Test error is triggered when the datadir in use contains a bitcoin.conf file that would be ignored '
292+
'because a conflicting -conf file argument is passed.')
293+
node = self.nodes[0]
294+
with tempfile.NamedTemporaryFile(dir=self.options.tmpdir, mode="wt", delete=False) as temp_conf:
295+
temp_conf.write(f"datadir={node.datadir}\n")
296+
node.assert_start_raises_init_error([f"-conf={temp_conf.name}"], re.escape(
297+
f'Error: Data directory "{node.datadir}" contains a "bitcoin.conf" file which is ignored, because a '
298+
f'different configuration file "{temp_conf.name}" from command line argument "-conf={temp_conf.name}" '
299+
f'is being used instead.') + r"[\s\S]*", match=ErrorMatch.FULL_REGEX)
300+
301+
# Test that passing a redundant -conf command line argument pointing to
302+
# the same bitcoin.conf that would be loaded anyway does not trigger an
303+
# error.
304+
self.start_node(0, [f'-conf={node.datadir}/bitcoin.conf'])
305+
self.stop_node(0)
306+
307+
def test_ignored_default_conf(self):
308+
# Disable this test for windows currently because trying to override
309+
# the default datadir through the environment does not seem to work.
310+
if sys.platform == "win32":
311+
return
312+
313+
self.log.info('Test error is triggered when bitcoin.conf in the default data directory sets another datadir '
314+
'and it contains a different bitcoin.conf file that would be ignored')
315+
316+
# Create a temporary directory that will be treated as the default data
317+
# directory by bitcoind.
318+
env, default_datadir = util.get_temp_default_datadir(pathlib.Path(self.options.tmpdir, "home"))
319+
default_datadir.mkdir(parents=True)
320+
321+
# Write a bitcoin.conf file in the default data directory containing a
322+
# datadir= line pointing at the node datadir. This will trigger a
323+
# startup error because the node datadir contains a different
324+
# bitcoin.conf that would be ignored.
325+
node = self.nodes[0]
326+
(default_datadir / "bitcoin.conf").write_text(f"datadir={node.datadir}\n")
327+
328+
# Drop the node -datadir= argument during this test, because if it is
329+
# specified it would take precedence over the datadir setting in the
330+
# config file.
331+
node_args = node.args
332+
node.args = [arg for arg in node.args if not arg.startswith("-datadir=")]
333+
node.assert_start_raises_init_error([], re.escape(
334+
f'Error: Data directory "{node.datadir}" contains a "bitcoin.conf" file which is ignored, because a '
335+
f'different configuration file "{default_datadir}/bitcoin.conf" from data directory "{default_datadir}" '
336+
f'is being used instead.') + r"[\s\S]*", env=env, match=ErrorMatch.FULL_REGEX)
337+
node.args = node_args
338+
285339
def run_test(self):
286340
self.test_log_buffer()
287341
self.test_args_log()
@@ -291,6 +345,8 @@ def run_test(self):
291345

292346
self.test_config_file_parser()
293347
self.test_invalid_command_line_options()
348+
self.test_ignored_conf()
349+
self.test_ignored_default_conf()
294350

295351
# Remove the -datadir argument so it doesn't override the config file
296352
self.nodes[0].args = [arg for arg in self.nodes[0].args if not arg.startswith("-datadir")]

test/functional/test_framework/test_node.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ def __getattr__(self, name):
190190
assert self.rpc_connected and self.rpc is not None, self._node_msg("Error: no RPC connection")
191191
return getattr(RPCOverloadWrapper(self.rpc, descriptors=self.descriptors), name)
192192

193-
def start(self, extra_args=None, *, cwd=None, stdout=None, stderr=None, **kwargs):
193+
def start(self, extra_args=None, *, cwd=None, stdout=None, stderr=None, env=None, **kwargs):
194194
"""Start the node."""
195195
if extra_args is None:
196196
extra_args = self.extra_args
@@ -213,6 +213,8 @@ def start(self, extra_args=None, *, cwd=None, stdout=None, stderr=None, **kwargs
213213

214214
# add environment variable LIBC_FATAL_STDERR_=1 so that libc errors are written to stderr and not the terminal
215215
subp_env = dict(os.environ, LIBC_FATAL_STDERR_="1")
216+
if env is not None:
217+
subp_env.update(env)
216218

217219
self.process = subprocess.Popen(self.args + extra_args, env=subp_env, stdout=stdout, stderr=stderr, cwd=cwd, **kwargs)
218220

test/functional/test_framework/util.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@
1212
import json
1313
import logging
1414
import os
15+
import pathlib
1516
import random
1617
import re
18+
import sys
1719
import time
1820
import unittest
1921

2022
from . import coverage
2123
from .authproxy import AuthServiceProxy, JSONRPCException
22-
from typing import Callable, Optional
24+
from typing import Callable, Optional, Tuple
2325

2426
logger = logging.getLogger("TestFramework.utils")
2527

@@ -420,6 +422,22 @@ def get_datadir_path(dirname, n):
420422
return os.path.join(dirname, "node" + str(n))
421423

422424

425+
def get_temp_default_datadir(temp_dir: pathlib.Path) -> Tuple[dict, pathlib.Path]:
426+
"""Return os-specific environment variables that can be set to make the
427+
GetDefaultDataDir() function return a datadir path under the provided
428+
temp_dir, as well as the complete path it would return."""
429+
if sys.platform == "win32":
430+
env = dict(APPDATA=str(temp_dir))
431+
datadir = temp_dir / "Bitcoin"
432+
else:
433+
env = dict(HOME=str(temp_dir))
434+
if sys.platform == "darwin":
435+
datadir = temp_dir / "Library/Application Support/Bitcoin"
436+
else:
437+
datadir = temp_dir / ".bitcoin"
438+
return env, datadir
439+
440+
423441
def append_config(datadir, options):
424442
with open(os.path.join(datadir, "bitcoin.conf"), 'a', encoding='utf8') as f:
425443
for option in options:

0 commit comments

Comments
 (0)