Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/release-notes-27302.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Configuration
---

- `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.
5 changes: 5 additions & 0 deletions doc/release-notes-28414.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
RPC Wallet
----------

- RPC `walletprocesspsbt` return object now includes field `hex` (if the transaction
is complete) containing the serialized transaction suitable for RPC `sendrawtransaction`. (#28414)
26 changes: 19 additions & 7 deletions src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ static const char* DEFAULT_ASMAP_FILENAME="ip_asn.map";
* The PID file facilities.
*/
static const char* BITCOIN_PID_FILENAME = "dashd.pid";
/**
* True if this process has created a PID file.
* Used to determine whether we should remove the PID file on shutdown.
*/
static bool g_generated_pid{false};

static fs::path GetPidFile(const ArgsManager& args)
{
Expand All @@ -198,12 +203,24 @@ static fs::path GetPidFile(const ArgsManager& args)
#else
tfm::format(file, "%d\n", getpid());
#endif
g_generated_pid = true;
return true;
} else {
return InitError(strprintf(_("Unable to create the PID file '%s': %s"), fs::PathToString(GetPidFile(args)), SysErrorString(errno)));
}
}

static void RemovePidFile(const ArgsManager& args)
{
if (!g_generated_pid) return;
const auto pid_path{GetPidFile(args)};
if (std::error_code error; !fs::remove(pid_path, error)) {
std::string msg{error ? error.message() : "File does not exist"};
LogPrintf("Unable to remove PID file (%s): %s\n", fs::PathToString(pid_path), msg);
}
}


//////////////////////////////////////////////////////////////////////////////
//
// Shutdown
Expand Down Expand Up @@ -475,13 +492,7 @@ void Shutdown(NodeContext& node)
node.chainman.reset();
node.scheduler.reset();

try {
if (!fs::remove(GetPidFile(*node.args))) {
LogPrintf("%s: Unable to remove PID file: File does not exist\n", __func__);
}
} catch (const fs::filesystem_error& e) {
LogPrintf("%s: Unable to remove PID file: %s\n", __func__, fsbridge::get_filesystem_error_message(e));
}
RemovePidFile(*node.args);

LogPrintf("%s: done\n", __func__);
}
Expand Down Expand Up @@ -580,6 +591,7 @@ void SetupServerArgs(ArgsManager& argsman)
argsman.AddArg("-dbbatchsize", strprintf("Maximum database write batch size in bytes (default: %u)", nDefaultDbBatchSize), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::OPTIONS);
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);
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);
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);
argsman.AddArg("-loadblock=<file>", "Imports blocks from external file on startup", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-maxmempool=<n>", strprintf("Keep the transaction memory pool below <n> megabytes (default: %u)", DEFAULT_MAX_MEMPOOL_SIZE), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-maxorphantxsize=<n>", strprintf("Maximum total size of all orphan transactions in megabytes (default: %u)", DEFAULT_MAX_ORPHAN_TRANSACTIONS_SIZE), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
Expand Down
4 changes: 4 additions & 0 deletions src/qt/test/optiontests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ void OptionTests::migrateSettings()
QVERIFY(!settings.contains("addrSeparateProxyTor"));

std::ifstream file(gArgs.GetDataDirNet() / "settings.json");
std::string default_warning = strprintf("This file is automatically generated and updated by %s. Please do not edit this file while the node "
"is running, as any changes might be ignored or overwritten.",
PACKAGE_NAME);
QCOMPARE(std::string(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()).c_str(), "{\n"
" \"_warning_\": \""+ default_warning+"\",\n"
" \"dbcache\": \"600\",\n"
" \"listen\": false,\n"
" \"onion\": \"onion:234\",\n"
Expand Down
3 changes: 3 additions & 0 deletions src/qt/test/test_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ int main(int argc, char* argv[])
gArgs.ForceSetArg("-upnp", "0");
gArgs.ForceSetArg("-natpmp", "0");

std::string error;
if (!gArgs.ReadConfigFiles(error, true)) QWARN(error.c_str());

// Prefer the "minimal" platform for the test instead of the normal default
// platform ("xcb", "windows", or "cocoa") so tests can't unintentionally
// interfere with any background GUIs and don't require extra resources.
Expand Down
19 changes: 14 additions & 5 deletions src/rpc/mining.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,30 @@ using node::UpdateTime;

/**
* Return average network hashes per second based on the last 'lookup' blocks,
* or from the last difficulty change if 'lookup' is nonpositive.
* If 'height' is nonnegative, compute the estimate at the time when a given block was found.
* or from the last difficulty change if 'lookup' is -1.
* If 'height' is -1, compute the estimate from current chain tip.
* If 'height' is a valid block height, compute the estimate at the time when a given block was found.
*/
static UniValue GetNetworkHashPS(int lookup, int height, const CChain& active_chain) {
if (lookup < -1 || lookup == 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid nblocks. Must be a positive number or -1.");
}

if (height < -1 || height > active_chain.Height()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Block does not exist at specified height");
}

const CBlockIndex* pb = active_chain.Tip();

if (height >= 0 && height < active_chain.Height()) {
if (height >= 0) {
pb = active_chain[height];
}

if (pb == nullptr || !pb->nHeight)
return 0;

// If lookup is -1, then use blocks since last difficulty change.
if (lookup <= 0)
if (lookup == -1)
lookup = pb->nHeight % Params().GetConsensus().DifficultyAdjustmentInterval() + 1;

// If lookup is larger than chain, then set it to chain length.
Expand Down Expand Up @@ -102,7 +111,7 @@ static RPCHelpMan getnetworkhashps()
"Pass in [blocks] to override # of blocks, -1 specifies since last difficulty change.\n"
"Pass in [height] to estimate the network speed at the time when a certain block was found.\n",
{
{"nblocks", RPCArg::Type::NUM, RPCArg::Default{120}, "The number of blocks, or -1 for blocks since last difficulty change."},
{"nblocks", RPCArg::Type::NUM, RPCArg::Default{120}, "The number of previous blocks to calculate estimate from, or -1 for blocks since last difficulty change."},
{"height", RPCArg::Type::NUM, RPCArg::Default{-1}, "To estimate at the time of the given height."},
},
RPCResult{
Expand Down
4 changes: 3 additions & 1 deletion src/test/settings_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ BOOST_AUTO_TEST_CASE(ReadWrite)
// Check invalid json not allowed
WriteText(path, R"(invalid json)");
BOOST_CHECK(!util::ReadSettings(path, values, errors));
std::vector<std::string> fail_parse = {strprintf("Unable to parse settings file %s", fs::PathToString(path))};
std::vector<std::string> fail_parse = {strprintf("Settings file %s does not contain valid JSON. This is probably caused by disk corruption or a crash, "
"and can be fixed by removing the file, which will reset settings to default values.",
fs::PathToString(path))};
BOOST_CHECK_EQUAL_COLLECTIONS(errors.begin(), errors.end(), fail_parse.begin(), fail_parse.end());
}

Expand Down
15 changes: 14 additions & 1 deletion src/util/settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
#include <fs.h>
#include <util/settings.h>

#if defined(HAVE_CONFIG_H)
#include <config/bitcoin-config.h>
#endif

#include <tinyformat.h>
#include <univalue.h>

Expand Down Expand Up @@ -90,7 +94,9 @@ bool ReadSettings(const fs::path& path, std::map<std::string, SettingsValue>& va

SettingsValue in;
if (!in.read(std::string{std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()})) {
errors.emplace_back(strprintf("Unable to parse settings file %s", fs::PathToString(path)));
errors.emplace_back(strprintf("Settings file %s does not contain valid JSON. This is probably caused by disk corruption or a crash, "
"and can be fixed by removing the file, which will reset settings to default values.",
fs::PathToString(path)));
return false;
}

Expand Down Expand Up @@ -123,6 +129,13 @@ bool WriteSettings(const fs::path& path,
std::vector<std::string>& errors)
{
SettingsValue out(SettingsValue::VOBJ);
// Add auto-generated warning comment only if it does not exist
if (!values.contains("_warning_")) {
out.pushKV("_warning_", strprintf("This file is automatically generated and updated by %s. Please do not edit this file while the node "
"is running, as any changes might be ignored or overwritten.",
PACKAGE_NAME));
}
// Push settings values
for (const auto& value : values) {
out.__pushKV(value.first, value.second);
}
Expand Down
8 changes: 6 additions & 2 deletions src/wallet/rpc/encrypt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,11 @@ RPCHelpMan encryptwallet()
"After this, any calls that interact with private keys such as sending or signing \n"
"will require the passphrase to be set prior the making these calls.\n"
"Use the walletpassphrase call for this, and then walletlock call.\n"
"If the wallet is already encrypted, use the walletpassphrasechange call.\n",
"If the wallet is already encrypted, use the walletpassphrasechange call.\n"
"** IMPORTANT **\n"
"For security reasons, the encryption process will generate a new HD seed, resulting\n"
"in the creation of a fresh set of active descriptors. Therefore, it is crucial to\n"
"securely back up the newly generated wallet file using the backupwallet RPC.\n",
{
{"passphrase", RPCArg::Type::STR, RPCArg::Optional::NO, "The pass phrase to encrypt the wallet with. It must be at least 1 character, but should be long."},
},
Expand Down Expand Up @@ -266,7 +270,7 @@ RPCHelpMan encryptwallet()
if (pwallet->IsHDEnabled()) {
return "wallet encrypted; If you forget the passphrase, you will lose access to your funds. Make sure that you have backup of your seed or mnemonic.";
}
return "wallet encrypted; The keypool has been flushed. You need to make a new backup.";
return "wallet encrypted; The keypool has been flushed. You need to make a new backup with the backupwallet RPC.";
},
};
}
Expand Down
9 changes: 9 additions & 0 deletions src/wallet/rpc/spend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1278,6 +1278,7 @@ RPCHelpMan walletprocesspsbt()
{
{RPCResult::Type::STR, "psbt", "The base64-encoded partially signed transaction"},
{RPCResult::Type::BOOL, "complete", "If the transaction has a complete set of signatures"},
{RPCResult::Type::STR_HEX, "hex", /*optional=*/true, "The hex-encoded network transaction if complete"},
}
},
RPCExamples{
Expand Down Expand Up @@ -1327,6 +1328,14 @@ RPCHelpMan walletprocesspsbt()
ssTx << psbtx;
result.pushKV("psbt", EncodeBase64(ssTx.str()));
result.pushKV("complete", complete);
if (complete) {
CMutableTransaction mtx;
// Returns true if complete, which we already think it is.
CHECK_NONFATAL(FinalizeAndExtractPSBT(psbtx, mtx));
CDataStream ssTx_final(SER_NETWORK, PROTOCOL_VERSION);
ssTx_final << mtx;
result.pushKV("hex", HexStr(ssTx_final));
}

return result;
},
Expand Down
94 changes: 93 additions & 1 deletion test/functional/feature_config_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@
"""Test various command line arguments and configuration file parameters."""

import os
import pathlib
import re
import sys
import tempfile
import time

from test_framework.test_framework import BitcoinTestFramework
from test_framework.test_node import ErrorMatch
from test_framework import util


Expand Down Expand Up @@ -74,7 +79,7 @@ def test_config_file_parser(self):
util.write_config(main_conf_file_path, n=0, chain='', extra_config=f'includeconf={inc_conf_file_path}\n')
with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
conf.write('acceptnonstdtxn=1\n')
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')
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')

with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
conf.write('nono\n')
Expand Down Expand Up @@ -108,6 +113,41 @@ def test_config_file_parser(self):
with open(inc_conf_file2_path, 'w', encoding='utf-8') as conf:
conf.write('') # clear

def test_config_file_log(self):
# Disable this test for windows currently because trying to override
# the default datadir through the environment does not seem to work.
if sys.platform == "win32":
return

self.log.info('Test that correct configuration path is changed when configuration file changes the datadir')

# Create a temporary directory that will be treated as the default data
# directory by bitcoind.
env, default_datadir = util.get_temp_default_datadir(pathlib.Path(self.options.tmpdir, "test_config_file_log"))
default_datadir.mkdir(parents=True)

# Write a bitcoin.conf file in the default data directory containing a
# datadir= line pointing at the node datadir.
node = self.nodes[0]
conf_text = pathlib.Path(node.bitcoinconf).read_text()
conf_path = default_datadir / "bitcoin.conf"
conf_path.write_text(f"datadir={node.datadir}\n{conf_text}")

# Drop the node -datadir= argument during this test, because if it is
# specified it would take precedence over the datadir setting in the
# config file.
node_args = node.args
node.args = [arg for arg in node.args if not arg.startswith("-datadir=")]

# Check that correct configuration file path is actually logged
# (conf_path, not node.bitcoinconf)
with self.nodes[0].assert_debug_log(expected_msgs=[f"Config file: {conf_path}"]):
self.start_node(0, ["-allowignoredconf"], env=env)
self.stop_node(0)

# Restore node arguments after the test
node.args = node_args

def test_invalid_command_line_options(self):
self.nodes[0].assert_start_raises_init_error(
expected_msg='Error: Error parsing command line arguments: Can not set -proxy with no value. Please specify value with -proxy=value.',
Expand Down Expand Up @@ -278,6 +318,55 @@ def test_connect_with_seednode(self):
unexpected_msgs=seednode_ignored):
self.restart_node(0, extra_args=[connect_arg, '-seednode=fakeaddress2'])

def test_ignored_conf(self):
self.log.info('Test error is triggered when the datadir in use contains a bitcoin.conf file that would be ignored '
'because a conflicting -conf file argument is passed.')
node = self.nodes[0]
with tempfile.NamedTemporaryFile(dir=self.options.tmpdir, mode="wt", delete=False) as temp_conf:
temp_conf.write(f"datadir={node.datadir}\n")
node.assert_start_raises_init_error([f"-conf={temp_conf.name}"], re.escape(
f'Error: Data directory "{node.datadir}" contains a "bitcoin.conf" file which is ignored, because a '
f'different configuration file "{temp_conf.name}" from command line argument "-conf={temp_conf.name}" '
f'is being used instead.') + r"[\s\S]*", match=ErrorMatch.FULL_REGEX)

# Test that passing a redundant -conf command line argument pointing to
# the same bitcoin.conf that would be loaded anyway does not trigger an
# error.
self.start_node(0, [f'-conf={node.datadir}/bitcoin.conf'])
self.stop_node(0)

def test_ignored_default_conf(self):
# Disable this test for windows currently because trying to override
# the default datadir through the environment does not seem to work.
if sys.platform == "win32":
return

self.log.info('Test error is triggered when bitcoin.conf in the default data directory sets another datadir '
'and it contains a different bitcoin.conf file that would be ignored')

# Create a temporary directory that will be treated as the default data
# directory by bitcoind.
env, default_datadir = util.get_temp_default_datadir(pathlib.Path(self.options.tmpdir, "home"))
default_datadir.mkdir(parents=True)

# Write a bitcoin.conf file in the default data directory containing a
# datadir= line pointing at the node datadir. This will trigger a
# startup error because the node datadir contains a different
# bitcoin.conf that would be ignored.
node = self.nodes[0]
(default_datadir / "bitcoin.conf").write_text(f"datadir={node.datadir}\n")

# Drop the node -datadir= argument during this test, because if it is
# specified it would take precedence over the datadir setting in the
# config file.
node_args = node.args
node.args = [arg for arg in node.args if not arg.startswith("-datadir=")]
node.assert_start_raises_init_error([], re.escape(
f'Error: Data directory "{node.datadir}" contains a "bitcoin.conf" file which is ignored, because a '
f'different configuration file "{default_datadir}/bitcoin.conf" from data directory "{default_datadir}" '
f'is being used instead.') + r"[\s\S]*", env=env, match=ErrorMatch.FULL_REGEX)
node.args = node_args

def run_test(self):
self.test_log_buffer()
self.test_args_log()
Expand All @@ -287,7 +376,10 @@ def run_test(self):


self.test_config_file_parser()
self.test_config_file_log()
self.test_invalid_command_line_options()
self.test_ignored_conf()
self.test_ignored_default_conf()

# Remove the -datadir argument so it doesn't override the config file
self.nodes[0].args = [arg for arg in self.nodes[0].args if not arg.startswith("-datadir")]
Expand Down
5 changes: 4 additions & 1 deletion test/functional/feature_filelock.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ def run_test(self):
expected_msg = f"Error: Cannot obtain a lock on data directory {datadir}. {self.config['environment']['PACKAGE_NAME']} is probably already running."
self.nodes[1].assert_start_raises_init_error(extra_args=[f'-datadir={self.nodes[0].datadir}', '-noserver'], expected_msg=expected_msg)

self.log.info("Check that cookie and PID file are not deleted when attempting to start a second dashd using the same datadir")
cookie_file = Path(datadir) / ".cookie"
assert cookie_file.exists() # should not be deleted during the second bitcoind instance shutdown
assert cookie_file.exists() # should not be deleted during the second dashd instance shutdown
pid_file = datadir / "dashd.pid"
assert pid_file.exists()

if self.is_wallet_compiled():
def check_wallet_filelock(descriptors):
Expand Down
Loading
Loading