diff --git a/doc/release-notes.md b/doc/release-notes.md
index 221b6b23a..06c4dddc5 100644
--- a/doc/release-notes.md
+++ b/doc/release-notes.md
@@ -1,29 +1,38 @@
Bitcoin ABC version 0.19.7 is now available from:
This release includes the following features and fixes:
- `-includeconf=` can be used to include additional configuration files.
Only works inside the `bitcoin.conf` file, not inside included files or from
command-line. Multiple files may be included. Can be disabled from command-
line via `-noincludeconf`. Note that multi-argument commands like
`-includeconf` will override preceding `-noincludeconf`, i.e.
noincludeconf=1
includeconf=relative.conf
as bitcoin.conf will still include `relative.conf`.
- The `createrawtransaction` RPC will now accept an array or dictionary (kept for compatibility) for the `outputs` parameter. This means the order of transaction outputs can be specified by the client.
- The new RPC `testmempoolaccept` can be used to test acceptance of a transaction to the mempool without adding it.
- An `initialblockdownload` boolean has been added to the `getblockchaininfo` RPC to indicate whether the node is currently in IBD or not.
- The '-usehd' option has been removed. It is no longer possible to create a non HD wallet.
+External wallet files
+---------------------
+
+The `-wallet=` option now accepts full paths instead of requiring wallets
+to be located in the -walletdir directory. When wallets are located in
+different directories, wallet data will be stored independently, so data from
+every wallet is not mixed into the same /database/log.??????????
+files.
+
Transaction index changes
-------------------------
The transaction index is now built separately from the main node procedure,
meaning the `-txindex` flag can be toggled without a full reindex. If bitcoind
is run with `-txindex` on a node that is already partially or fully synced
without one, the transaction index will be built in the background and become
available once caught up. When switching from running `-txindex` to running
without the flag, the transaction index database will *not* be deleted
automatically, meaning it could be turned back on at a later time without a full
resync.
diff --git a/src/bitcoin-cli.cpp b/src/bitcoin-cli.cpp
index fbf9f71bd..587ca034f 100644
--- a/src/bitcoin-cli.cpp
+++ b/src/bitcoin-cli.cpp
@@ -1,604 +1,604 @@
// Copyright (c) 2009-2010 Satoshi Nakamoto
// Copyright (c) 2009-2016 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#if defined(HAVE_CONFIG_H)
#include
#endif
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
static const char DEFAULT_RPCCONNECT[] = "127.0.0.1";
static const int DEFAULT_HTTP_CLIENT_TIMEOUT = 900;
static const bool DEFAULT_NAMED = false;
static const int CONTINUE_EXECUTION = -1;
static void SetupCliArgs() {
const auto defaultBaseParams =
CreateBaseChainParams(CBaseChainParams::MAIN);
const auto testnetBaseParams =
CreateBaseChainParams(CBaseChainParams::TESTNET);
gArgs.AddArg("-?", _("This help message"), false, OptionsCategory::OPTIONS);
gArgs.AddArg("-conf=",
strprintf(_("Specify configuration file (default: %s)"),
BITCOIN_CONF_FILENAME),
false, OptionsCategory::OPTIONS);
gArgs.AddArg("-datadir=", _("Specify data directory"), false,
OptionsCategory::OPTIONS);
gArgs.AddArg(
"-getinfo",
_("Get general information from the remote server. Note that unlike "
"server-side RPC calls, the results of -getinfo is the result of "
"multiple non-atomic requests. Some entries in the result may "
"represent results from different states (e.g. wallet balance may be "
"as of a different block from the chain state reported)"),
false, OptionsCategory::OPTIONS);
SetupChainParamsBaseOptions();
gArgs.AddArg(
"-named",
strprintf(_("Pass named instead of positional arguments (default: %s)"),
DEFAULT_NAMED),
false, OptionsCategory::OPTIONS);
gArgs.AddArg(
"-rpcconnect=",
strprintf(_("Send commands to node running on (default: %s)"),
DEFAULT_RPCCONNECT),
false, OptionsCategory::OPTIONS);
gArgs.AddArg(
"-rpcport=",
strprintf(
_("Connect to JSON-RPC on (default: %u or testnet: %u)"),
defaultBaseParams->RPCPort(), testnetBaseParams->RPCPort()),
false, OptionsCategory::OPTIONS);
gArgs.AddArg("-rpcwait", _("Wait for RPC server to start"), false,
OptionsCategory::OPTIONS);
gArgs.AddArg("-rpcuser=", _("Username for JSON-RPC connections"),
false, OptionsCategory::OPTIONS);
gArgs.AddArg("-rpcpassword=", _("Password for JSON-RPC connections"),
false, OptionsCategory::OPTIONS);
gArgs.AddArg("-rpcclienttimeout=",
strprintf(_("Timeout in seconds during HTTP requests, or 0 "
"for no timeout. (default: %d)"),
DEFAULT_HTTP_CLIENT_TIMEOUT),
false, OptionsCategory::OPTIONS);
gArgs.AddArg(
"-stdinrpcpass",
strprintf(_("Read RPC password from standard input as a single line. "
"When combined with -stdin, the first line from standard "
"input is used for the RPC password.")),
false, OptionsCategory::OPTIONS);
gArgs.AddArg("-stdin",
_("Read extra arguments from standard input, one per line "
"until EOF/Ctrl-D (recommended for sensitive information "
"such as passphrases)"),
false, OptionsCategory::OPTIONS);
- gArgs.AddArg("-rpcwallet=",
- _("Send RPC for non-default wallet on RPC server (argument is "
- "wallet filename in bitcoind directory, required if "
- "bitcoind/-Qt runs with multiple wallets)"),
- false, OptionsCategory::OPTIONS);
+ gArgs.AddArg(
+ "-rpcwallet=",
+ _("Send RPC for non-default wallet on RPC server (needs to exactly "
+ "match corresponding -wallet option passed to bitcoind)"),
+ false, OptionsCategory::OPTIONS);
}
//////////////////////////////////////////////////////////////////////////////
//
// Start
//
//
// Exception thrown on connection error. This error is used to determine when
// to wait if -rpcwait is given.
//
class CConnectionFailed : public std::runtime_error {
public:
explicit inline CConnectionFailed(const std::string &msg)
: std::runtime_error(msg) {}
};
//
// This function returns either one of EXIT_ codes when it's expected to stop
// the process or CONTINUE_EXECUTION when it's expected to continue further.
//
static int AppInitRPC(int argc, char *argv[]) {
//
// Parameters
//
SetupCliArgs();
gArgs.ParseParameters(argc, argv);
if (argc < 2 || HelpRequested(gArgs) || gArgs.IsArgSet("-version")) {
std::string strUsage =
PACKAGE_NAME " RPC client version " + FormatFullVersion() + "\n";
if (!gArgs.IsArgSet("-version")) {
strUsage += "\n"
"Usage: bitcoin-cli [options] [params] "
"Send command to " PACKAGE_NAME "\n"
"or: bitcoin-cli [options] -named "
"[name=value]... Send command to " PACKAGE_NAME
" (with named arguments)\n"
"or: bitcoin-cli [options] help "
"List commands\n"
"or: bitcoin-cli [options] help Get "
"help for a command\n";
strUsage += "\n" + gArgs.GetHelpMessage();
}
fprintf(stdout, "%s", strUsage.c_str());
if (argc < 2) {
fprintf(stderr, "Error: too few parameters\n");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
if (!fs::is_directory(GetDataDir(false))) {
fprintf(stderr,
"Error: Specified data directory \"%s\" does not exist.\n",
gArgs.GetArg("-datadir", "").c_str());
return EXIT_FAILURE;
}
try {
gArgs.ReadConfigFiles();
} catch (const std::exception &e) {
fprintf(stderr, "Error reading configuration file: %s\n", e.what());
return EXIT_FAILURE;
}
// Check for -testnet or -regtest parameter (BaseParams() calls are only
// valid after this clause)
try {
SelectBaseParams(gArgs.GetChainName());
} catch (const std::exception &e) {
fprintf(stderr, "Error: %s\n", e.what());
return EXIT_FAILURE;
}
if (gArgs.GetBoolArg("-rpcssl", false)) {
fprintf(stderr,
"Error: SSL mode for RPC (-rpcssl) is no longer supported.\n");
return EXIT_FAILURE;
}
return CONTINUE_EXECUTION;
}
/** Reply structure for request_done to fill in */
struct HTTPReply {
HTTPReply() : status(0), error(-1) {}
int status;
int error;
std::string body;
};
const char *http_errorstring(int code) {
switch (code) {
#if LIBEVENT_VERSION_NUMBER >= 0x02010300
case EVREQ_HTTP_TIMEOUT:
return "timeout reached";
case EVREQ_HTTP_EOF:
return "EOF reached";
case EVREQ_HTTP_INVALID_HEADER:
return "error while reading header, or invalid header";
case EVREQ_HTTP_BUFFER_ERROR:
return "error encountered while reading or writing";
case EVREQ_HTTP_REQUEST_CANCEL:
return "request was canceled";
case EVREQ_HTTP_DATA_TOO_LONG:
return "response body is larger than allowed";
#endif
default:
return "unknown";
}
}
static void http_request_done(struct evhttp_request *req, void *ctx) {
HTTPReply *reply = static_cast(ctx);
if (req == nullptr) {
/**
* If req is nullptr, it means an error occurred while connecting: the
* error code will have been passed to http_error_cb.
*/
reply->status = 0;
return;
}
reply->status = evhttp_request_get_response_code(req);
struct evbuffer *buf = evhttp_request_get_input_buffer(req);
if (buf) {
size_t size = evbuffer_get_length(buf);
const char *data = (const char *)evbuffer_pullup(buf, size);
if (data) reply->body = std::string(data, size);
evbuffer_drain(buf, size);
}
}
#if LIBEVENT_VERSION_NUMBER >= 0x02010300
static void http_error_cb(enum evhttp_request_error err, void *ctx) {
HTTPReply *reply = static_cast(ctx);
reply->error = err;
}
#endif
/**
* Class that handles the conversion from a command-line to a JSON-RPC request,
* as well as converting back to a JSON object that can be shown as result.
*/
class BaseRequestHandler {
public:
virtual UniValue PrepareRequest(const std::string &method,
const std::vector &args) = 0;
virtual UniValue ProcessReply(const UniValue &batch_in) = 0;
};
/** Process getinfo requests */
class GetinfoRequestHandler : public BaseRequestHandler {
public:
const int ID_NETWORKINFO = 0;
const int ID_BLOCKCHAININFO = 1;
const int ID_WALLETINFO = 2;
/** Create a simulated `getinfo` request. */
UniValue PrepareRequest(const std::string &method,
const std::vector &args) override {
UniValue result(UniValue::VARR);
result.push_back(
JSONRPCRequestObj("getnetworkinfo", NullUniValue, ID_NETWORKINFO));
result.push_back(JSONRPCRequestObj("getblockchaininfo", NullUniValue,
ID_BLOCKCHAININFO));
result.push_back(
JSONRPCRequestObj("getwalletinfo", NullUniValue, ID_WALLETINFO));
return result;
}
/** Collect values from the batch and form a simulated `getinfo` reply. */
UniValue ProcessReply(const UniValue &batch_in) override {
UniValue result(UniValue::VOBJ);
std::vector batch = JSONRPCProcessBatchReply(batch_in, 3);
// Errors in getnetworkinfo() and getblockchaininfo() are fatal, pass
// them on getwalletinfo() is allowed to fail in case there is no
// wallet.
if (!batch[ID_NETWORKINFO]["error"].isNull()) {
return batch[ID_NETWORKINFO];
}
if (!batch[ID_BLOCKCHAININFO]["error"].isNull()) {
return batch[ID_BLOCKCHAININFO];
}
result.pushKV("version", batch[ID_NETWORKINFO]["result"]["version"]);
result.pushKV("protocolversion",
batch[ID_NETWORKINFO]["result"]["protocolversion"]);
if (!batch[ID_WALLETINFO].isNull()) {
result.pushKV("walletversion",
batch[ID_WALLETINFO]["result"]["walletversion"]);
result.pushKV("balance", batch[ID_WALLETINFO]["result"]["balance"]);
}
result.pushKV("blocks", batch[ID_BLOCKCHAININFO]["result"]["blocks"]);
result.pushKV("timeoffset",
batch[ID_NETWORKINFO]["result"]["timeoffset"]);
result.pushKV("connections",
batch[ID_NETWORKINFO]["result"]["connections"]);
result.pushKV("proxy",
batch[ID_NETWORKINFO]["result"]["networks"][0]["proxy"]);
result.pushKV("difficulty",
batch[ID_BLOCKCHAININFO]["result"]["difficulty"]);
result.pushKV(
"testnet",
UniValue(batch[ID_BLOCKCHAININFO]["result"]["chain"].get_str() ==
"test"));
if (!batch[ID_WALLETINFO].isNull()) {
result.pushKV("walletversion",
batch[ID_WALLETINFO]["result"]["walletversion"]);
result.pushKV("balance", batch[ID_WALLETINFO]["result"]["balance"]);
result.pushKV("keypoololdest",
batch[ID_WALLETINFO]["result"]["keypoololdest"]);
result.pushKV("keypoolsize",
batch[ID_WALLETINFO]["result"]["keypoolsize"]);
if (!batch[ID_WALLETINFO]["result"]["unlocked_until"].isNull()) {
result.pushKV("unlocked_until",
batch[ID_WALLETINFO]["result"]["unlocked_until"]);
}
result.pushKV("paytxfee",
batch[ID_WALLETINFO]["result"]["paytxfee"]);
}
result.pushKV("relayfee", batch[ID_NETWORKINFO]["result"]["relayfee"]);
result.pushKV("warnings", batch[ID_NETWORKINFO]["result"]["warnings"]);
return JSONRPCReplyObj(result, NullUniValue, 1);
}
};
/** Process default single requests */
class DefaultRequestHandler : public BaseRequestHandler {
public:
UniValue PrepareRequest(const std::string &method,
const std::vector &args) override {
UniValue params;
if (gArgs.GetBoolArg("-named", DEFAULT_NAMED)) {
params = RPCConvertNamedValues(method, args);
} else {
params = RPCConvertValues(method, args);
}
return JSONRPCRequestObj(method, params, 1);
}
UniValue ProcessReply(const UniValue &reply) override {
return reply.get_obj();
}
};
static UniValue CallRPC(BaseRequestHandler *rh, const std::string &strMethod,
const std::vector &args) {
std::string host;
// In preference order, we choose the following for the port:
// 1. -rpcport
// 2. port in -rpcconnect (ie following : in ipv4 or ]: in ipv6)
// 3. default port for chain
int port = BaseParams().RPCPort();
SplitHostPort(gArgs.GetArg("-rpcconnect", DEFAULT_RPCCONNECT), port, host);
port = gArgs.GetArg("-rpcport", port);
// Obtain event base
raii_event_base base = obtain_event_base();
// Synchronously look up hostname
raii_evhttp_connection evcon =
obtain_evhttp_connection_base(base.get(), host, port);
evhttp_connection_set_timeout(
evcon.get(),
gArgs.GetArg("-rpcclienttimeout", DEFAULT_HTTP_CLIENT_TIMEOUT));
HTTPReply response;
raii_evhttp_request req =
obtain_evhttp_request(http_request_done, (void *)&response);
if (req == nullptr) throw std::runtime_error("create http request failed");
#if LIBEVENT_VERSION_NUMBER >= 0x02010300
evhttp_request_set_error_cb(req.get(), http_error_cb);
#endif
// Get credentials
std::string strRPCUserColonPass;
if (gArgs.GetArg("-rpcpassword", "") == "") {
// Try fall back to cookie-based authentication if no password is
// provided
if (!GetAuthCookie(&strRPCUserColonPass)) {
throw std::runtime_error(strprintf(
_("Could not locate RPC credentials. No authentication cookie "
"could be found, and RPC password is not set. See "
"-rpcpassword and -stdinrpcpass. Configuration file: (%s)"),
GetConfigFile(gArgs.GetArg("-conf", BITCOIN_CONF_FILENAME))
.string()
.c_str()));
}
} else {
strRPCUserColonPass = gArgs.GetArg("-rpcuser", "") + ":" +
gArgs.GetArg("-rpcpassword", "");
}
struct evkeyvalq *output_headers =
evhttp_request_get_output_headers(req.get());
assert(output_headers);
evhttp_add_header(output_headers, "Host", host.c_str());
evhttp_add_header(output_headers, "Connection", "close");
evhttp_add_header(
output_headers, "Authorization",
(std::string("Basic ") + EncodeBase64(strRPCUserColonPass)).c_str());
// Attach request data
std::string strRequest = rh->PrepareRequest(strMethod, args).write() + "\n";
struct evbuffer *output_buffer =
evhttp_request_get_output_buffer(req.get());
assert(output_buffer);
evbuffer_add(output_buffer, strRequest.data(), strRequest.size());
// check if we should use a special wallet endpoint
std::string endpoint = "/";
std::string walletName = gArgs.GetArg("-rpcwallet", "");
if (!walletName.empty()) {
char *encodedURI =
evhttp_uriencode(walletName.c_str(), walletName.size(), false);
if (encodedURI) {
endpoint = "/wallet/" + std::string(encodedURI);
free(encodedURI);
} else {
throw CConnectionFailed("uri-encode failed");
}
}
int r = evhttp_make_request(evcon.get(), req.get(), EVHTTP_REQ_POST,
endpoint.c_str());
// ownership moved to evcon in above call
req.release();
if (r != 0) {
throw CConnectionFailed("send http request failed");
}
event_base_dispatch(base.get());
if (response.status == 0) {
throw CConnectionFailed(strprintf(
"couldn't connect to server: %s (code %d)\n(make sure server is "
"running and you are connecting to the correct RPC port)",
http_errorstring(response.error), response.error));
} else if (response.status == HTTP_UNAUTHORIZED) {
throw std::runtime_error(
"incorrect rpcuser or rpcpassword (authorization failed)");
} else if (response.status >= 400 && response.status != HTTP_BAD_REQUEST &&
response.status != HTTP_NOT_FOUND &&
response.status != HTTP_INTERNAL_SERVER_ERROR) {
throw std::runtime_error(
strprintf("server returned HTTP error %d", response.status));
} else if (response.body.empty()) {
throw std::runtime_error("no response from server");
}
// Parse reply
UniValue valReply(UniValue::VSTR);
if (!valReply.read(response.body)) {
throw std::runtime_error("couldn't parse reply from server");
}
const UniValue reply = rh->ProcessReply(valReply);
if (reply.empty()) {
throw std::runtime_error(
"expected reply to have result, error and id properties");
}
return reply;
}
int CommandLineRPC(int argc, char *argv[]) {
std::string strPrint;
int nRet = 0;
try {
// Skip switches
while (argc > 1 && IsSwitchChar(argv[1][0])) {
argc--;
argv++;
}
std::string rpcPass;
if (gArgs.GetBoolArg("-stdinrpcpass", false)) {
if (!std::getline(std::cin, rpcPass))
throw std::runtime_error("-stdinrpcpass specified but failed "
"to read from standard input");
gArgs.ForceSetArg("-rpcpassword", rpcPass);
}
std::vector args =
std::vector(&argv[1], &argv[argc]);
if (gArgs.GetBoolArg("-stdin", false)) {
// Read one arg per line from stdin and append
std::string line;
while (std::getline(std::cin, line)) {
args.push_back(line);
}
}
std::unique_ptr rh;
std::string method;
if (gArgs.GetBoolArg("-getinfo", false)) {
rh.reset(new GetinfoRequestHandler());
method = "";
} else {
rh.reset(new DefaultRequestHandler());
if (args.size() < 1) {
throw std::runtime_error(
"too few parameters (need at least command)");
}
method = args[0];
// Remove trailing method name from arguments vector
args.erase(args.begin());
}
// Execute and handle connection failures with -rpcwait
const bool fWait = gArgs.GetBoolArg("-rpcwait", false);
do {
try {
const UniValue reply = CallRPC(rh.get(), method, args);
// Parse reply
const UniValue &result = find_value(reply, "result");
const UniValue &error = find_value(reply, "error");
if (!error.isNull()) {
// Error
int code = error["code"].get_int();
if (fWait && code == RPC_IN_WARMUP)
throw CConnectionFailed("server in warmup");
strPrint = "error: " + error.write();
nRet = abs(code);
if (error.isObject()) {
UniValue errCode = find_value(error, "code");
UniValue errMsg = find_value(error, "message");
strPrint =
errCode.isNull()
? ""
: "error code: " + errCode.getValStr() + "\n";
if (errMsg.isStr()) {
strPrint += "error message:\n" + errMsg.get_str();
}
if (errCode.isNum() &&
errCode.get_int() == RPC_WALLET_NOT_SPECIFIED) {
strPrint += "\nTry adding "
"\"-rpcwallet=\" option to "
"bitcoin-cli command line.";
}
}
} else {
// Result
if (result.isNull()) {
strPrint = "";
} else if (result.isStr()) {
strPrint = result.get_str();
} else {
strPrint = result.write(2);
}
}
// Connection succeeded, no need to retry.
break;
} catch (const CConnectionFailed &) {
if (fWait) {
MilliSleep(1000);
} else {
throw;
}
}
} while (fWait);
} catch (const boost::thread_interrupted &) {
throw;
} catch (const std::exception &e) {
strPrint = std::string("error: ") + e.what();
nRet = EXIT_FAILURE;
} catch (...) {
PrintExceptionContinue(nullptr, "CommandLineRPC()");
throw;
}
if (strPrint != "") {
fprintf((nRet == 0 ? stdout : stderr), "%s\n", strPrint.c_str());
}
return nRet;
}
int main(int argc, char *argv[]) {
SetupEnvironment();
if (!SetupNetworking()) {
fprintf(stderr, "Error: Initializing networking failed\n");
return EXIT_FAILURE;
}
try {
int ret = AppInitRPC(argc, argv);
if (ret != CONTINUE_EXECUTION) {
return ret;
}
} catch (const std::exception &e) {
PrintExceptionContinue(&e, "AppInitRPC()");
return EXIT_FAILURE;
} catch (...) {
PrintExceptionContinue(nullptr, "AppInitRPC()");
return EXIT_FAILURE;
}
int ret = EXIT_FAILURE;
try {
ret = CommandLineRPC(argc, argv);
} catch (const std::exception &e) {
PrintExceptionContinue(&e, "CommandLineRPC()");
} catch (...) {
PrintExceptionContinue(nullptr, "CommandLineRPC()");
}
return ret;
}
diff --git a/src/wallet/db.cpp b/src/wallet/db.cpp
index 502041539..565066d6a 100644
--- a/src/wallet/db.cpp
+++ b/src/wallet/db.cpp
@@ -1,846 +1,847 @@
// Copyright (c) 2009-2010 Satoshi Nakamoto
// Copyright (c) 2009-2016 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include
#include
#include
#include
#include
#include
#include
#include
#include // boost::this_thread::interruption_point() (mingw)
#include
#include
#ifndef WIN32
#include
#endif
namespace {
//! Make sure database has a unique fileid within the environment. If it
//! doesn't, throw an error. BDB caches do not work properly when more than one
//! open database has the same fileid (values written to one database may show
//! up in reads to other databases).
//!
//! BerkeleyDB generates unique fileids by default
//! (https://docs.oracle.com/cd/E17275_01/html/programmer_reference/program_copy.html),
//! so bitcoin should never create different databases with the same fileid, but
//! this error can be triggered if users manually copy database files.
void CheckUniqueFileid(const CDBEnv &env, const std::string &filename, Db &db) {
if (env.IsMock()) {
return;
}
u_int8_t fileid[DB_FILE_ID_LEN];
int ret = db.get_mpf()->get_fileid(fileid);
if (ret != 0) {
throw std::runtime_error(
strprintf("CDB: Can't open database %s (get_fileid failed with %d)",
filename, ret));
}
for (const auto &item : env.mapDb) {
u_int8_t item_fileid[DB_FILE_ID_LEN];
if (item.second &&
item.second->get_mpf()->get_fileid(item_fileid) == 0 &&
memcmp(fileid, item_fileid, sizeof(fileid)) == 0) {
const char *item_filename = nullptr;
item.second->get_dbname(&item_filename, nullptr);
throw std::runtime_error(strprintf(
"CDB: Can't open database %s (duplicates fileid %s from %s)",
filename,
HexStr(std::begin(item_fileid), std::end(item_fileid)),
item_filename ? item_filename : "(unknown database)"));
}
}
}
CCriticalSection cs_db;
//!< Map from directory name to open db environment.
std::map g_dbenvs;
} // namespace
CDBEnv *GetWalletEnv(const fs::path &wallet_path,
std::string &database_filename) {
fs::path env_directory = wallet_path.parent_path();
database_filename = wallet_path.filename().string();
LOCK(cs_db);
// Note: An ununsed temporary CDBEnv object may be created inside the
// emplace function if the key already exists. This is a little inefficient,
// but not a big concern since the map will be changed in the future to hold
// pointers instead of objects, anyway.
return &g_dbenvs
.emplace(std::piecewise_construct,
std::forward_as_tuple(env_directory.string()),
std::forward_as_tuple(env_directory))
.first->second;
}
//
// CDB
//
void CDBEnv::Close() {
if (!fDbEnvInit) {
return;
}
fDbEnvInit = false;
for (auto &db : mapDb) {
auto count = mapFileUseCount.find(db.first);
assert(count == mapFileUseCount.end() || count->second == 0);
if (db.second) {
db.second->close(0);
delete db.second;
db.second = nullptr;
}
}
int ret = dbenv->close(0);
if (ret != 0) {
LogPrintf("CDBEnv::EnvShutdown: Error %d shutting down database "
"environment: %s\n",
ret, DbEnv::strerror(ret));
}
if (!fMockDb) {
DbEnv(uint32_t(0)).remove(strPath.c_str(), 0);
}
}
void CDBEnv::Reset() {
dbenv.reset(new DbEnv(DB_CXX_NO_EXCEPTIONS));
fDbEnvInit = false;
fMockDb = false;
}
CDBEnv::CDBEnv(const fs::path &dir_path) : strPath(dir_path.string()) {
Reset();
}
CDBEnv::~CDBEnv() {
Close();
}
bool CDBEnv::Open(bool retry) {
if (fDbEnvInit) {
return true;
}
boost::this_thread::interruption_point();
fs::path pathIn = strPath;
+ TryCreateDirectories(pathIn);
if (!LockDirectory(pathIn, ".walletlock")) {
LogPrintf("Cannot obtain a lock on wallet directory %s. Another "
"instance of bitcoin may be using it.\n",
strPath);
return false;
}
fs::path pathLogDir = pathIn / "database";
TryCreateDirectories(pathLogDir);
fs::path pathErrorFile = pathIn / "db.log";
LogPrintf("CDBEnv::Open: LogDir=%s ErrorFile=%s\n", pathLogDir.string(),
pathErrorFile.string());
unsigned int nEnvFlags = 0;
if (gArgs.GetBoolArg("-privdb", DEFAULT_WALLET_PRIVDB)) {
nEnvFlags |= DB_PRIVATE;
}
dbenv->set_lg_dir(pathLogDir.string().c_str());
// 1 MiB should be enough for just the wallet
dbenv->set_cachesize(0, 0x100000, 1);
dbenv->set_lg_bsize(0x10000);
dbenv->set_lg_max(1048576);
dbenv->set_lk_max_locks(40000);
dbenv->set_lk_max_objects(40000);
/// debug
dbenv->set_errfile(fsbridge::fopen(pathErrorFile, "a"));
dbenv->set_flags(DB_AUTO_COMMIT, 1);
dbenv->set_flags(DB_TXN_WRITE_NOSYNC, 1);
dbenv->log_set_config(DB_LOG_AUTO_REMOVE, 1);
int ret =
dbenv->open(strPath.c_str(),
DB_CREATE | DB_INIT_LOCK | DB_INIT_LOG | DB_INIT_MPOOL |
DB_INIT_TXN | DB_THREAD | DB_RECOVER | nEnvFlags,
S_IRUSR | S_IWUSR);
if (ret != 0) {
dbenv->close(0);
LogPrintf("CDBEnv::Open: Error %d opening database environment: %s\n",
ret, DbEnv::strerror(ret));
if (retry) {
// try moving the database env out of the way
fs::path pathDatabaseBak =
pathIn / strprintf("database.%d.bak", GetTime());
try {
fs::rename(pathLogDir, pathDatabaseBak);
LogPrintf("Moved old %s to %s. Retrying.\n",
pathLogDir.string(), pathDatabaseBak.string());
} catch (const fs::filesystem_error &) {
// failure is ok (well, not really, but it's not worse than what
// we started with)
}
// try opening it again one more time
if (!Open(false /* retry */)) {
// if it still fails, it probably means we can't even create the
// database env
return false;
}
} else {
return false;
}
}
fDbEnvInit = true;
fMockDb = false;
return true;
}
void CDBEnv::MakeMock() {
if (fDbEnvInit) {
throw std::runtime_error("CDBEnv::MakeMock: Already initialized");
}
boost::this_thread::interruption_point();
LogPrint(BCLog::DB, "CDBEnv::MakeMock\n");
dbenv->set_cachesize(1, 0, 1);
dbenv->set_lg_bsize(10485760 * 4);
dbenv->set_lg_max(10485760);
dbenv->set_lk_max_locks(10000);
dbenv->set_lk_max_objects(10000);
dbenv->set_flags(DB_AUTO_COMMIT, 1);
dbenv->log_set_config(DB_LOG_IN_MEMORY, 1);
int ret =
dbenv->open(nullptr,
DB_CREATE | DB_INIT_LOCK | DB_INIT_LOG | DB_INIT_MPOOL |
DB_INIT_TXN | DB_THREAD | DB_PRIVATE,
S_IRUSR | S_IWUSR);
if (ret > 0) {
throw std::runtime_error(strprintf(
"CDBEnv::MakeMock: Error %d opening database environment.", ret));
}
fDbEnvInit = true;
fMockDb = true;
}
CDBEnv::VerifyResult CDBEnv::Verify(const std::string &strFile,
recoverFunc_type recoverFunc,
std::string &out_backup_filename) {
LOCK(cs_db);
assert(mapFileUseCount.count(strFile) == 0);
Db db(dbenv.get(), 0);
int result = db.verify(strFile.c_str(), nullptr, nullptr, 0);
if (result == 0) {
return VerifyResult::VERIFY_OK;
} else if (recoverFunc == nullptr) {
return VerifyResult::RECOVER_FAIL;
}
// Try to recover:
bool fRecovered =
(*recoverFunc)(fs::path(strPath) / strFile, out_backup_filename);
return (fRecovered ? VerifyResult::RECOVER_OK : VerifyResult::RECOVER_FAIL);
}
bool CDB::Recover(const fs::path &file_path, void *callbackDataIn,
bool (*recoverKVcallback)(void *callbackData,
CDataStream ssKey,
CDataStream ssValue),
std::string &newFilename) {
std::string filename;
CDBEnv *env = GetWalletEnv(file_path, filename);
// Recovery procedure:
// Move wallet file to walletfilename.timestamp.bak
// Call Salvage with fAggressive=true to get as much data as possible.
// Rewrite salvaged data to fresh wallet file.
// Set -rescan so any missing transactions will be found.
int64_t now = GetTime();
newFilename = strprintf("%s.%d.bak", filename, now);
int result = env->dbenv->dbrename(nullptr, filename.c_str(), nullptr,
newFilename.c_str(), DB_AUTO_COMMIT);
if (result == 0) {
LogPrintf("Renamed %s to %s\n", filename, newFilename);
} else {
LogPrintf("Failed to rename %s to %s\n", filename, newFilename);
return false;
}
std::vector salvagedData;
bool fSuccess = env->Salvage(newFilename, true, salvagedData);
if (salvagedData.empty()) {
LogPrintf("Salvage(aggressive) found no records in %s.\n", newFilename);
return false;
}
LogPrintf("Salvage(aggressive) found %u records\n", salvagedData.size());
std::unique_ptr pdbCopy = std::make_unique(env->dbenv.get(), 0);
int ret = pdbCopy->open(nullptr, // Txn pointer
filename.c_str(), // Filename
"main", // Logical db name
DB_BTREE, // Database type
DB_CREATE, // Flags
0);
if (ret > 0) {
LogPrintf("Cannot create database file %s\n", filename);
pdbCopy->close(0);
return false;
}
DbTxn *ptxn = env->TxnBegin();
for (CDBEnv::KeyValPair &row : salvagedData) {
if (recoverKVcallback) {
CDataStream ssKey(row.first, SER_DISK, CLIENT_VERSION);
CDataStream ssValue(row.second, SER_DISK, CLIENT_VERSION);
std::string strType, strErr;
if (!(*recoverKVcallback)(callbackDataIn, ssKey, ssValue)) {
continue;
}
}
Dbt datKey(&row.first[0], row.first.size());
Dbt datValue(&row.second[0], row.second.size());
int ret2 = pdbCopy->put(ptxn, &datKey, &datValue, DB_NOOVERWRITE);
if (ret2 > 0) {
fSuccess = false;
}
}
ptxn->commit(0);
pdbCopy->close(0);
return fSuccess;
}
bool CDB::VerifyEnvironment(const fs::path &file_path, std::string &errorStr) {
std::string walletFile;
CDBEnv *env = GetWalletEnv(file_path, walletFile);
fs::path walletDir = env->Directory();
LogPrintf("Using BerkeleyDB version %s\n", DbEnv::version(0, 0, 0));
LogPrintf("Using wallet %s\n", walletFile);
// Wallet file must be a plain filename without a directory
if (walletFile != fs::basename(walletFile) + fs::extension(walletFile)) {
errorStr = strprintf(_("Wallet %s resides outside wallet directory %s"),
walletFile, walletDir.string());
return false;
}
if (!env->Open(true /* retry */)) {
errorStr = strprintf(
_("Error initializing wallet database environment %s!"), walletDir);
return false;
}
return true;
}
bool CDB::VerifyDatabaseFile(const fs::path &file_path, std::string &warningStr,
std::string &errorStr,
CDBEnv::recoverFunc_type recoverFunc) {
std::string walletFile;
CDBEnv *env = GetWalletEnv(file_path, walletFile);
fs::path walletDir = env->Directory();
if (fs::exists(walletDir / walletFile)) {
std::string backup_filename;
CDBEnv::VerifyResult r =
env->Verify(walletFile, recoverFunc, backup_filename);
if (r == CDBEnv::VerifyResult::RECOVER_OK) {
warningStr = strprintf(
_("Warning: Wallet file corrupt, data salvaged!"
" Original %s saved as %s in %s; if"
" your balance or transactions are incorrect you should"
" restore from a backup."),
walletFile, backup_filename, walletDir);
}
if (r == CDBEnv::VerifyResult::RECOVER_FAIL) {
errorStr = strprintf(_("%s corrupt, salvage failed"), walletFile);
return false;
}
}
// also return true if files does not exists
return true;
}
/* End of headers, beginning of key/value data */
static const char *HEADER_END = "HEADER=END";
/* End of key/value data */
static const char *DATA_END = "DATA=END";
bool CDBEnv::Salvage(const std::string &strFile, bool fAggressive,
std::vector &vResult) {
LOCK(cs_db);
assert(mapFileUseCount.count(strFile) == 0);
u_int32_t flags = DB_SALVAGE;
if (fAggressive) {
flags |= DB_AGGRESSIVE;
}
std::stringstream strDump;
Db db(dbenv.get(), 0);
int result = db.verify(strFile.c_str(), nullptr, &strDump, flags);
if (result == DB_VERIFY_BAD) {
LogPrintf("CDBEnv::Salvage: Database salvage found errors, all data "
"may not be recoverable.\n");
if (!fAggressive) {
LogPrintf("CDBEnv::Salvage: Rerun with aggressive mode to ignore "
"errors and continue.\n");
return false;
}
}
if (result != 0 && result != DB_VERIFY_BAD) {
LogPrintf("CDBEnv::Salvage: Database salvage failed with result %d.\n",
result);
return false;
}
// Format of bdb dump is ascii lines:
// header lines...
// HEADER=END
// hexadecimal key
// hexadecimal value
// ... repeated
// DATA=END
std::string strLine;
while (!strDump.eof() && strLine != HEADER_END) {
// Skip past header
getline(strDump, strLine);
}
std::string keyHex, valueHex;
while (!strDump.eof() && keyHex != DATA_END) {
getline(strDump, keyHex);
if (keyHex != DATA_END) {
if (strDump.eof()) {
break;
}
getline(strDump, valueHex);
if (valueHex == DATA_END) {
LogPrintf("CDBEnv::Salvage: WARNING: Number of keys in data "
"does not match number of values.\n");
break;
}
vResult.push_back(make_pair(ParseHex(keyHex), ParseHex(valueHex)));
}
}
if (keyHex != DATA_END) {
LogPrintf("CDBEnv::Salvage: WARNING: Unexpected end of file while "
"reading salvage output.\n");
return false;
}
return (result == 0);
}
void CDBEnv::CheckpointLSN(const std::string &strFile) {
dbenv->txn_checkpoint(0, 0, 0);
if (fMockDb) {
return;
}
dbenv->lsn_reset(strFile.c_str(), 0);
}
CDB::CDB(CWalletDBWrapper &dbw, const char *pszMode, bool fFlushOnCloseIn)
: pdb(nullptr), activeTxn(nullptr) {
int ret;
fReadOnly = (!strchr(pszMode, '+') && !strchr(pszMode, 'w'));
fFlushOnClose = fFlushOnCloseIn;
env = dbw.env;
if (dbw.IsDummy()) {
return;
}
const std::string &strFilename = dbw.strFile;
bool fCreate = strchr(pszMode, 'c') != nullptr;
unsigned int nFlags = DB_THREAD;
if (fCreate) {
nFlags |= DB_CREATE;
}
{
LOCK(cs_db);
if (!env->Open(false /* retry */)) {
throw std::runtime_error(
"CDB: Failed to open database environment.");
}
pdb = env->mapDb[strFilename];
if (pdb == nullptr) {
std::unique_ptr pdb_temp =
std::make_unique(env->dbenv.get(), 0);
bool fMockDb = env->IsMock();
if (fMockDb) {
DbMpoolFile *mpf = pdb_temp->get_mpf();
ret = mpf->set_flags(DB_MPOOL_NOFILE, 1);
if (ret != 0) {
throw std::runtime_error(
strprintf("CDB: Failed to configure for no temp file "
"backing for database %s",
strFilename));
}
}
ret = pdb_temp->open(
nullptr, // Txn pointer
fMockDb ? nullptr : strFilename.c_str(), // Filename
fMockDb ? strFilename.c_str() : "main", // Logical db name
DB_BTREE, // Database type
nFlags, // Flags
0);
if (ret != 0) {
throw std::runtime_error(strprintf(
"CDB: Error %d, can't open database %s", ret, strFilename));
}
// Call CheckUniqueFileid on the containing BDB environment to
// avoid BDB data consistency bugs that happen when different data
// files in the same environment have the same fileid.
//
// Also call CheckUniqueFileid on all the other g_dbenvs to prevent
// bitcoin from opening the same data file through another
// environment when the file is referenced through equivalent but
// not obviously identical symlinked or hard linked or bind mounted
// paths. In the future a more relaxed check for equal inode and
// device ids could be done instead, which would allow opening
// different backup copies of a wallet at the same time. Maybe even
// more ideally, an exclusive lock for accessing the database could
// be implemented, so no equality checks are needed at all. (Newer
// versions of BDB have an set_lk_exclusive method for this
// purpose, but the older version we use does not.)
for (auto &dbenv : g_dbenvs) {
CheckUniqueFileid(dbenv.second, strFilename, *pdb_temp);
}
pdb = pdb_temp.release();
env->mapDb[strFilename] = pdb;
if (fCreate && !Exists(std::string("version"))) {
bool fTmp = fReadOnly;
fReadOnly = false;
WriteVersion(CLIENT_VERSION);
fReadOnly = fTmp;
}
}
++env->mapFileUseCount[strFilename];
strFile = strFilename;
}
}
void CDB::Flush() {
if (activeTxn) {
return;
}
// Flush database activity from memory pool to disk log
unsigned int nMinutes = 0;
if (fReadOnly) {
nMinutes = 1;
}
env->dbenv->txn_checkpoint(
nMinutes ? gArgs.GetArg("-dblogsize", DEFAULT_WALLET_DBLOGSIZE) * 1024
: 0,
nMinutes, 0);
}
void CWalletDBWrapper::IncrementUpdateCounter() {
++nUpdateCounter;
}
void CDB::Close() {
if (!pdb) {
return;
}
if (activeTxn) {
activeTxn->abort();
}
activeTxn = nullptr;
pdb = nullptr;
if (fFlushOnClose) {
Flush();
}
LOCK(cs_db);
--env->mapFileUseCount[strFile];
}
void CDBEnv::CloseDb(const std::string &strFile) {
LOCK(cs_db);
if (mapDb[strFile] != nullptr) {
// Close the database handle
Db *pdb = mapDb[strFile];
pdb->close(0);
delete pdb;
mapDb[strFile] = nullptr;
}
}
bool CDB::Rewrite(CWalletDBWrapper &dbw, const char *pszSkip) {
if (dbw.IsDummy()) {
return true;
}
CDBEnv *env = dbw.env;
const std::string &strFile = dbw.strFile;
while (true) {
{
LOCK(cs_db);
if (!env->mapFileUseCount.count(strFile) ||
env->mapFileUseCount[strFile] == 0) {
// Flush log data to the dat file
env->CloseDb(strFile);
env->CheckpointLSN(strFile);
env->mapFileUseCount.erase(strFile);
bool fSuccess = true;
LogPrintf("CDB::Rewrite: Rewriting %s...\n", strFile);
std::string strFileRes = strFile + ".rewrite";
{
// surround usage of db with extra {}
CDB db(dbw, "r");
std::unique_ptr pdbCopy =
std::make_unique(env->dbenv.get(), 0);
int ret = pdbCopy->open(nullptr, // Txn pointer
strFileRes.c_str(), // Filename
"main", // Logical db name
DB_BTREE, // Database type
DB_CREATE, // Flags
0);
if (ret > 0) {
LogPrintf(
"CDB::Rewrite: Can't create database file %s\n",
strFileRes);
fSuccess = false;
}
Dbc *pcursor = db.GetCursor();
if (pcursor)
while (fSuccess) {
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
CDataStream ssValue(SER_DISK, CLIENT_VERSION);
int ret1 = db.ReadAtCursor(pcursor, ssKey, ssValue);
if (ret1 == DB_NOTFOUND) {
pcursor->close();
break;
}
if (ret1 != 0) {
pcursor->close();
fSuccess = false;
break;
}
if (pszSkip &&
strncmp(ssKey.data(), pszSkip,
std::min(ssKey.size(),
strlen(pszSkip))) == 0) {
continue;
}
if (strncmp(ssKey.data(), "\x07version", 8) == 0) {
// Update version:
ssValue.clear();
ssValue << CLIENT_VERSION;
}
Dbt datKey(ssKey.data(), ssKey.size());
Dbt datValue(ssValue.data(), ssValue.size());
int ret2 = pdbCopy->put(nullptr, &datKey, &datValue,
DB_NOOVERWRITE);
if (ret2 > 0) {
fSuccess = false;
}
}
if (fSuccess) {
db.Close();
env->CloseDb(strFile);
if (pdbCopy->close(0)) {
fSuccess = false;
}
} else {
pdbCopy->close(0);
}
}
if (fSuccess) {
Db dbA(env->dbenv.get(), 0);
if (dbA.remove(strFile.c_str(), nullptr, 0)) {
fSuccess = false;
}
Db dbB(env->dbenv.get(), 0);
if (dbB.rename(strFileRes.c_str(), nullptr, strFile.c_str(),
0)) {
fSuccess = false;
}
}
if (!fSuccess) {
LogPrintf(
"CDB::Rewrite: Failed to rewrite database file %s\n",
strFileRes);
}
return fSuccess;
}
}
MilliSleep(100);
}
return false;
}
void CDBEnv::Flush(bool fShutdown) {
int64_t nStart = GetTimeMillis();
// Flush log data to the actual data file on all files that are not in use
LogPrint(BCLog::DB, "CDBEnv::Flush: Flush(%s)%s\n",
fShutdown ? "true" : "false",
fDbEnvInit ? "" : " database not started");
if (!fDbEnvInit) {
return;
}
{
LOCK(cs_db);
std::map::iterator mi = mapFileUseCount.begin();
while (mi != mapFileUseCount.end()) {
std::string strFile = (*mi).first;
int nRefCount = (*mi).second;
LogPrint(BCLog::DB,
"CDBEnv::Flush: Flushing %s (refcount = %d)...\n", strFile,
nRefCount);
if (nRefCount == 0) {
// Move log data to the dat file
CloseDb(strFile);
LogPrint(BCLog::DB, "CDBEnv::Flush: %s checkpoint\n", strFile);
dbenv->txn_checkpoint(0, 0, 0);
LogPrint(BCLog::DB, "CDBEnv::Flush: %s detach\n", strFile);
if (!fMockDb) {
dbenv->lsn_reset(strFile.c_str(), 0);
}
LogPrint(BCLog::DB, "CDBEnv::Flush: %s closed\n", strFile);
mapFileUseCount.erase(mi++);
} else {
mi++;
}
}
LogPrint(BCLog::DB, "CDBEnv::Flush: Flush(%s)%s took %15dms\n",
fShutdown ? "true" : "false",
fDbEnvInit ? "" : " database not started",
GetTimeMillis() - nStart);
if (fShutdown) {
char **listp;
if (mapFileUseCount.empty()) {
dbenv->log_archive(&listp, DB_ARCH_REMOVE);
Close();
if (!fMockDb) {
fs::remove_all(fs::path(strPath) / "database");
}
}
}
}
}
bool CDB::PeriodicFlush(CWalletDBWrapper &dbw) {
if (dbw.IsDummy()) {
return true;
}
bool ret = false;
CDBEnv *env = dbw.env;
const std::string &strFile = dbw.strFile;
TRY_LOCK(cs_db, lockDb);
if (lockDb) {
// Don't do this if any databases are in use
int nRefCount = 0;
std::map::iterator mit = env->mapFileUseCount.begin();
while (mit != env->mapFileUseCount.end()) {
nRefCount += (*mit).second;
mit++;
}
if (nRefCount == 0) {
boost::this_thread::interruption_point();
std::map::iterator mi =
env->mapFileUseCount.find(strFile);
if (mi != env->mapFileUseCount.end()) {
LogPrint(BCLog::DB, "Flushing %s\n", strFile);
int64_t nStart = GetTimeMillis();
// Flush wallet file so it's self contained
env->CloseDb(strFile);
env->CheckpointLSN(strFile);
env->mapFileUseCount.erase(mi++);
LogPrint(BCLog::DB, "Flushed %s %dms\n", strFile,
GetTimeMillis() - nStart);
ret = true;
}
}
}
return ret;
}
bool CWalletDBWrapper::Rewrite(const char *pszSkip) {
return CDB::Rewrite(*this, pszSkip);
}
bool CWalletDBWrapper::Backup(const std::string &strDest) {
if (IsDummy()) {
return false;
}
while (true) {
{
LOCK(cs_db);
if (!env->mapFileUseCount.count(strFile) ||
env->mapFileUseCount[strFile] == 0) {
// Flush log data to the dat file
env->CloseDb(strFile);
env->CheckpointLSN(strFile);
env->mapFileUseCount.erase(strFile);
// Copy wallet file.
fs::path pathSrc = GetWalletDir() / strFile;
fs::path pathDest(strDest);
if (fs::is_directory(pathDest)) {
pathDest /= strFile;
}
try {
if (fs::equivalent(pathSrc, pathDest)) {
LogPrintf("cannot backup to wallet source file %s\n",
pathDest.string());
return false;
}
fs::copy_file(pathSrc, pathDest,
fs::copy_option::overwrite_if_exists);
LogPrintf("copied %s to %s\n", strFile, pathDest.string());
return true;
} catch (const fs::filesystem_error &e) {
LogPrintf("error copying %s to %s - %s\n", strFile,
pathDest.string(), e.what());
return false;
}
}
}
MilliSleep(100);
}
return false;
}
void CWalletDBWrapper::Flush(bool shutdown) {
if (!IsDummy()) {
env->Flush(shutdown);
}
}
diff --git a/src/wallet/init.cpp b/src/wallet/init.cpp
index ae1f4baf3..894239849 100644
--- a/src/wallet/init.cpp
+++ b/src/wallet/init.cpp
@@ -1,427 +1,417 @@
// Copyright (c) 2009-2010 Satoshi Nakamoto
// Copyright (c) 2009-2017 The Bitcoin Core developers
// Copyright (c) 2018 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
class WalletInit : public WalletInitInterface {
public:
//! Return the wallets help message.
void AddWalletOptions() const override;
//! Wallets parameter interaction
bool ParameterInteraction() const override;
//! Register wallet RPCs.
void RegisterRPC(CRPCTable &tableRPC) const override;
//! Responsible for reading and validating the -wallet arguments and
//! verifying the wallet database.
// This function will perform salvage on the wallet if requested, as long
// as only one wallet is being loaded (WalletParameterInteraction forbids
// -salvagewallet, -zapwallettxes or -upgradewallet with multiwallet).
bool Verify(const CChainParams &chainParams) const override;
//! Load wallet databases.
bool Open(const CChainParams &chainParams) const override;
//! Complete startup of wallets.
void Start(CScheduler &scheduler) const override;
//! Flush all wallets in preparation for shutdown.
void Flush() const override;
//! Stop all wallets. Wallets will be flushed first.
void Stop() const override;
//! Close all wallets.
void Close() const override;
};
const WalletInitInterface &g_wallet_init_interface = WalletInit();
void WalletInit::AddWalletOptions() const {
gArgs.AddArg("-disablewallet",
_("Do not load the wallet and disable wallet RPC calls"),
false, OptionsCategory::WALLET);
gArgs.AddArg("-keypool=",
strprintf(_("Set key pool size to (default: %u)"),
DEFAULT_KEYPOOL_SIZE),
false, OptionsCategory::WALLET);
gArgs.AddArg("-fallbackfee=",
strprintf(_("A fee rate (in %s/kB) that will be used when fee "
"estimation has insufficient data (default: %s)"),
CURRENCY_UNIT, FormatMoney(DEFAULT_FALLBACK_FEE)),
false, OptionsCategory::WALLET);
gArgs.AddArg(
"-paytxfee=",
strprintf(
_("Fee (in %s/kB) to add to transactions you send (default: %s)"),
CURRENCY_UNIT, FormatMoney(payTxFee.GetFeePerK())),
false, OptionsCategory::WALLET);
gArgs.AddArg(
"-rescan",
_("Rescan the block chain for missing wallet transactions on startup"),
false, OptionsCategory::WALLET);
gArgs.AddArg(
"-salvagewallet",
_("Attempt to recover private keys from a corrupt wallet on startup"),
false, OptionsCategory::WALLET);
gArgs.AddArg("-spendzeroconfchange",
strprintf(_("Spend unconfirmed change when sending "
"transactions (default: %d)"),
DEFAULT_SPEND_ZEROCONF_CHANGE),
false, OptionsCategory::WALLET);
gArgs.AddArg("-upgradewallet",
_("Upgrade wallet to latest format on startup"), false,
OptionsCategory::WALLET);
- gArgs.AddArg("-wallet=",
- _("Specify wallet file (within data directory)") + " " +
- strprintf(_("(default: %s)"), DEFAULT_WALLET_DAT),
- false, OptionsCategory::WALLET);
+ gArgs.AddArg(
+ "-wallet=",
+ _("Specify wallet database path. Can be specified multiple times to "
+ "load multiple wallets. Path is interpreted relative to "
+ "if it is not absolute, and will be created if it does not exist.") +
+ " " + strprintf(_("(default: %s)"), DEFAULT_WALLET_DAT),
+ false, OptionsCategory::WALLET);
gArgs.AddArg("-walletbroadcast",
_("Make the wallet broadcast transactions") + " " +
strprintf(_("(default: %d)"), DEFAULT_WALLETBROADCAST),
false, OptionsCategory::WALLET);
gArgs.AddArg("-walletdir=",
_("Specify directory to hold wallets (default: "
"/wallets if it exists, otherwise )"),
false, OptionsCategory::WALLET);
gArgs.AddArg("-walletnotify=",
_("Execute command when a wallet transaction changes (%s in "
"cmd is replaced by TxID)"),
false, OptionsCategory::WALLET);
gArgs.AddArg("-zapwallettxes=",
_("Delete all wallet transactions and only recover those "
"parts of the blockchain through -rescan on startup") +
" " +
_("(1 = keep tx meta data e.g. account owner and payment "
"request information, 2 = drop tx meta data)"),
false, OptionsCategory::WALLET);
gArgs.AddArg("-dblogsize=",
strprintf("Flush wallet database activity from memory to disk "
"log every megabytes (default: %u)",
DEFAULT_WALLET_DBLOGSIZE),
true, OptionsCategory::WALLET_DEBUG_TEST);
gArgs.AddArg(
"-flushwallet",
strprintf("Run a thread to flush wallet periodically (default: %d)",
DEFAULT_FLUSHWALLET),
true, OptionsCategory::WALLET_DEBUG_TEST);
gArgs.AddArg("-privdb",
strprintf("Sets the DB_PRIVATE flag in the wallet db "
"environment (default: %d)",
DEFAULT_WALLET_PRIVDB),
true, OptionsCategory::WALLET_DEBUG_TEST);
gArgs.AddArg("-walletrejectlongchains",
strprintf(_("Wallet will not create transactions that violate "
"mempool chain limits (default: %d)"),
DEFAULT_WALLET_REJECT_LONG_CHAINS),
true, OptionsCategory::WALLET_DEBUG_TEST);
}
bool WalletInit::ParameterInteraction() const {
CFeeRate minRelayTxFee = GetConfig().GetMinFeePerKB();
gArgs.SoftSetArg("-wallet", DEFAULT_WALLET_DAT);
const bool is_multiwallet = gArgs.GetArgs("-wallet").size() > 1;
if (gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) {
return true;
}
if (gArgs.GetBoolArg("-blocksonly", DEFAULT_BLOCKSONLY) &&
gArgs.SoftSetBoolArg("-walletbroadcast", false)) {
LogPrintf("%s: parameter interaction: -blocksonly=1 -> setting "
"-walletbroadcast=0\n",
__func__);
}
if (gArgs.GetBoolArg("-salvagewallet", false) &&
gArgs.SoftSetBoolArg("-rescan", true)) {
if (is_multiwallet) {
return InitError(
strprintf("%s is only allowed with a single wallet file",
"-salvagewallet"));
}
// Rewrite just private keys: rescan to find transactions
LogPrintf("%s: parameter interaction: -salvagewallet=1 -> setting "
"-rescan=1\n",
__func__);
}
int zapwallettxes = gArgs.GetArg("-zapwallettxes", 0);
// -zapwallettxes implies dropping the mempool on startup
if (zapwallettxes != 0 && gArgs.SoftSetBoolArg("-persistmempool", false)) {
LogPrintf("%s: parameter interaction: -zapwallettxes=%s -> setting "
"-persistmempool=0\n",
__func__, zapwallettxes);
}
// -zapwallettxes implies a rescan
if (zapwallettxes != 0) {
if (is_multiwallet) {
return InitError(
strprintf("%s is only allowed with a single wallet file",
"-zapwallettxes"));
}
if (gArgs.SoftSetBoolArg("-rescan", true)) {
LogPrintf("%s: parameter interaction: -zapwallettxes=%s -> setting "
"-rescan=1\n",
__func__, zapwallettxes);
}
LogPrintf("%s: parameter interaction: -zapwallettxes= -> setting "
"-rescan=1\n",
__func__);
}
if (is_multiwallet) {
if (gArgs.GetBoolArg("-upgradewallet", false)) {
return InitError(
strprintf("%s is only allowed with a single wallet file",
"-upgradewallet"));
}
}
if (gArgs.GetBoolArg("-sysperms", false)) {
return InitError("-sysperms is not allowed in combination with enabled "
"wallet functionality");
}
if (gArgs.GetArg("-prune", 0) && gArgs.GetBoolArg("-rescan", false)) {
return InitError(
_("Rescans are not possible in pruned mode. You will need to use "
"-reindex which will download the whole blockchain again."));
}
if (minRelayTxFee.GetFeePerK() > HIGH_TX_FEE_PER_KB) {
InitWarning(
AmountHighWarn("-minrelaytxfee") + " " +
_("The wallet will avoid paying less than the minimum relay fee."));
}
if (gArgs.IsArgSet("-fallbackfee")) {
Amount nFeePerK = Amount::zero();
if (!ParseMoney(gArgs.GetArg("-fallbackfee", ""), nFeePerK)) {
return InitError(
strprintf(_("Invalid amount for -fallbackfee=: '%s'"),
gArgs.GetArg("-fallbackfee", "")));
}
if (nFeePerK > HIGH_TX_FEE_PER_KB) {
InitWarning(AmountHighWarn("-fallbackfee") + " " +
_("This is the transaction fee you may pay when fee "
"estimates are not available."));
}
CWallet::fallbackFee = CFeeRate(nFeePerK);
}
if (gArgs.IsArgSet("-paytxfee")) {
Amount nFeePerK = Amount::zero();
if (!ParseMoney(gArgs.GetArg("-paytxfee", ""), nFeePerK)) {
return InitError(
AmountErrMsg("paytxfee", gArgs.GetArg("-paytxfee", "")));
}
if (nFeePerK > HIGH_TX_FEE_PER_KB) {
InitWarning(AmountHighWarn("-paytxfee") + " " +
_("This is the transaction fee you will pay if you "
"send a transaction."));
}
payTxFee = CFeeRate(nFeePerK, 1000);
if (payTxFee < minRelayTxFee) {
return InitError(strprintf(
_("Invalid amount for -paytxfee=: '%s' (must "
"be at least %s)"),
gArgs.GetArg("-paytxfee", ""), minRelayTxFee.ToString()));
}
}
if (gArgs.IsArgSet("-maxtxfee")) {
Amount nMaxFee = Amount::zero();
if (!ParseMoney(gArgs.GetArg("-maxtxfee", ""), nMaxFee)) {
return InitError(
AmountErrMsg("maxtxfee", gArgs.GetArg("-maxtxfee", "")));
}
if (nMaxFee > HIGH_MAX_TX_FEE) {
InitWarning(_("-maxtxfee is set very high! Fees this large could "
"be paid on a single transaction."));
}
maxTxFee = nMaxFee;
if (CFeeRate(maxTxFee, 1000) < minRelayTxFee) {
return InitError(strprintf(
_("Invalid amount for -maxtxfee=: '%s' (must "
"be at least the minrelay fee of %s to prevent "
"stuck transactions)"),
gArgs.GetArg("-maxtxfee", ""), minRelayTxFee.ToString()));
}
}
bSpendZeroConfChange =
gArgs.GetBoolArg("-spendzeroconfchange", DEFAULT_SPEND_ZEROCONF_CHANGE);
g_address_type = OutputType::DEFAULT;
g_change_type = OutputType::DEFAULT;
return true;
}
void WalletInit::RegisterRPC(CRPCTable &t) const {
if (gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) {
return;
}
RegisterWalletRPCCommands(t);
RegisterDumpRPCCommands(t);
}
bool WalletInit::Verify(const CChainParams &chainParams) const {
if (gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) {
return true;
}
if (gArgs.IsArgSet("-walletdir")) {
fs::path wallet_dir = gArgs.GetArg("-walletdir", "");
if (!fs::exists(wallet_dir)) {
return InitError(
strprintf(_("Specified -walletdir \"%s\" does not exist"),
wallet_dir.string()));
} else if (!fs::is_directory(wallet_dir)) {
return InitError(
strprintf(_("Specified -walletdir \"%s\" is not a directory"),
wallet_dir.string()));
} else if (!wallet_dir.is_absolute()) {
return InitError(
strprintf(_("Specified -walletdir \"%s\" is a relative path"),
wallet_dir.string()));
}
}
LogPrintf("Using wallet directory %s\n", GetWalletDir().string());
uiInterface.InitMessage(_("Verifying wallet(s)..."));
// Keep track of each wallet absolute path to detect duplicates.
std::set wallet_paths;
for (const std::string &walletFile : gArgs.GetArgs("-wallet")) {
- if (fs::path(walletFile).filename() != walletFile) {
- return InitError(
- strprintf(_("Error loading wallet %s. -wallet parameter must "
- "only specify a filename (not a path)."),
- walletFile));
- }
-
- if (SanitizeString(walletFile, SAFE_CHARS_FILENAME) != walletFile) {
- return InitError(strprintf(_("Error loading wallet %s. Invalid "
- "characters in -wallet filename."),
- walletFile));
- }
-
fs::path wallet_path = fs::absolute(walletFile, GetWalletDir());
if (fs::exists(wallet_path) && (!fs::is_regular_file(wallet_path) ||
fs::is_symlink(wallet_path))) {
return InitError(strprintf(_("Error loading wallet %s. -wallet "
"filename must be a regular file."),
walletFile));
}
if (!wallet_paths.insert(wallet_path).second) {
return InitError(strprintf(_("Error loading wallet %s. Duplicate "
"-wallet filename specified."),
walletFile));
}
std::string strError;
if (!CWalletDB::VerifyEnvironment(wallet_path, strError)) {
return InitError(strError);
}
if (gArgs.GetBoolArg("-salvagewallet", false)) {
// Recover readable keypairs:
CWallet dummyWallet(chainParams, "dummy",
CWalletDBWrapper::CreateDummy());
std::string backup_filename;
if (!CWalletDB::Recover(wallet_path, (void *)&dummyWallet,
CWalletDB::RecoverKeysOnlyFilter,
backup_filename)) {
return false;
}
}
std::string strWarning;
bool dbV =
CWalletDB::VerifyDatabaseFile(wallet_path, strWarning, strError);
if (!strWarning.empty()) {
InitWarning(strWarning);
}
if (!dbV) {
InitError(strError);
return false;
}
}
return true;
}
bool WalletInit::Open(const CChainParams &chainParams) const {
if (gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) {
LogPrintf("Wallet disabled!\n");
return true;
}
for (const std::string &walletFile : gArgs.GetArgs("-wallet")) {
CWallet *const pwallet = CWallet::CreateWalletFromFile(
chainParams, walletFile, fs::absolute(walletFile, GetWalletDir()));
if (!pwallet) {
return false;
}
vpwallets.push_back(pwallet);
}
return true;
}
void WalletInit::Start(CScheduler &scheduler) const {
for (CWalletRef pwallet : vpwallets) {
pwallet->postInitProcess(scheduler);
}
}
void WalletInit::Flush() const {
for (CWalletRef pwallet : vpwallets) {
pwallet->Flush(false);
}
}
void WalletInit::Stop() const {
for (CWalletRef pwallet : vpwallets) {
pwallet->Flush(true);
}
}
void WalletInit::Close() const {
for (CWalletRef pwallet : vpwallets) {
delete pwallet;
}
vpwallets.clear();
}
diff --git a/test/functional/wallet_multiwallet.py b/test/functional/wallet_multiwallet.py
index 53aa575c7..ed437427e 100755
--- a/test/functional/wallet_multiwallet.py
+++ b/test/functional/wallet_multiwallet.py
@@ -1,155 +1,160 @@
#!/usr/bin/env python3
# Copyright (c) 2017 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test multiwallet.
Verify that a bitcoind node can load multiple wallet files
"""
import os
import shutil
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
assert_raises_rpc_error,
)
class MultiWalletTest(BitcoinTestFramework):
def set_test_params(self):
self.setup_clean_chain = True
self.num_nodes = 2
- self.extra_args = [
- ['-wallet=w1', '-wallet=w2', '-wallet=w3', '-wallet=w'], []]
self.supports_cli = True
def run_test(self):
node = self.nodes[0]
data_dir = lambda *p: os.path.join(node.datadir, 'regtest', *p)
wallet_dir = lambda *p: data_dir('wallets', *p)
def wallet(name): return node.get_wallet_rpc(name)
- assert_equal(set(node.listwallets()), {"w1", "w2", "w3", "w"})
-
+ # check wallet.dat is created
self.stop_nodes()
+ assert_equal(os.path.isfile(wallet_dir('wallet.dat')), True)
+
+ # restart node with a mix of wallet names:
+ # w1, w2, w3 - to verify new wallets created when non-existing paths specified
+ # w - to verify wallet name matching works when one wallet path is prefix of another
+ # sub/w5 - to verify relative wallet path is created correctly
+ # extern/w6 - to verify absolute wallet path is created correctly
+ # wallet.dat - to verify existing wallet file is loaded correctly
+ wallet_names = ['w1', 'w2', 'w3', 'w', 'sub/w5',
+ os.path.join(self.options.tmpdir, 'extern/w6'), 'wallet.dat']
+ extra_args = ['-wallet={}'.format(n) for n in wallet_names]
+ self.start_node(0, extra_args)
+ assert_equal(set(node.listwallets()), set(wallet_names))
+
+ # check that all requested wallets were created
+ self.stop_node(0)
+ for wallet_name in wallet_names:
+ assert_equal(os.path.isfile(wallet_dir(wallet_name)), True)
+
+ # should not initialize if wallet path can't be created
+ self.assert_start_raises_init_error(
+ 0, ['-wallet=wallet.dat/bad'], 'File exists')
self.assert_start_raises_init_error(
0, ['-walletdir=wallets'], 'Error: Specified -walletdir "wallets" does not exist')
self.assert_start_raises_init_error(
0, ['-walletdir=wallets'], 'Error: Specified -walletdir "wallets" is a relative path', cwd=data_dir())
self.assert_start_raises_init_error(
0, ['-walletdir=debug.log'], 'Error: Specified -walletdir "debug.log" is not a directory', cwd=data_dir())
# should not initialize if there are duplicate wallets
self.assert_start_raises_init_error(
0, ['-wallet=w1', '-wallet=w1'], 'Error loading wallet w1. Duplicate -wallet filename specified.')
# should not initialize if wallet file is a directory
os.mkdir(wallet_dir('w11'))
self.assert_start_raises_init_error(
0, ['-wallet=w11'], 'Error loading wallet w11. -wallet filename must be a regular file.')
# should not initialize if one wallet is a copy of another
shutil.copyfile(wallet_dir('w2'), wallet_dir('w22'))
self.assert_start_raises_init_error(
0, ['-wallet=w2', '-wallet=w22'], 'duplicates fileid')
# should not initialize if wallet file is a symlink
os.symlink(wallet_dir('w1'), wallet_dir('w12'))
self.assert_start_raises_init_error(
0, ['-wallet=w12'], 'Error loading wallet w12. -wallet filename must be a regular file.')
# should not initialize if the specified walletdir does not exist
self.assert_start_raises_init_error(
0, ['-walletdir=bad'], 'Error: Specified -walletdir "bad" does not exist')
# should not initialize if the specified walletdir is not a directory
not_a_dir = wallet_dir('notadir')
open(not_a_dir, 'a').close()
self.assert_start_raises_init_error(
0, ['-walletdir='+not_a_dir], 'Error: Specified -walletdir "' + not_a_dir + '" is not a directory')
# if wallets/ doesn't exist, datadir should be the default wallet dir
wallet_dir2 = data_dir('walletdir')
os.rename(wallet_dir(), wallet_dir2)
self.start_node(0, ['-wallet=w4', '-wallet=w5'])
assert_equal(set(node.listwallets()), {"w4", "w5"})
w5 = wallet("w5")
w5.generate(1)
# now if wallets/ exists again, but the rootdir is specified as the walletdir, w4 and w5 should still be loaded
os.rename(wallet_dir2, wallet_dir())
self.restart_node(0, ['-wallet=w4', '-wallet=w5',
'-walletdir=' + data_dir()])
assert_equal(set(node.listwallets()), {"w4", "w5"})
w5 = wallet("w5")
w5_info = w5.getwalletinfo()
assert_equal(w5_info['immature_balance'], 50)
competing_wallet_dir = os.path.join(
self.options.tmpdir, 'competing_walletdir')
os.mkdir(competing_wallet_dir)
self.restart_node(0, ['-walletdir='+competing_wallet_dir])
self.assert_start_raises_init_error(
1, ['-walletdir='+competing_wallet_dir], 'Error initializing wallet database environment')
- self.restart_node(0, self.extra_args[0])
+ self.restart_node(0, extra_args)
- w1 = wallet("w1")
- w2 = wallet("w2")
- w3 = wallet("w3")
- w4 = wallet("w")
+ wallets = [wallet(w) for w in wallet_names]
wallet_bad = wallet("bad")
- w1.generate(1)
+ # check wallet names and balances
+ wallets[0].generate(1)
+ for wallet_name, wallet in zip(wallet_names, wallets):
+ info = wallet.getwalletinfo()
+ assert_equal(info['immature_balance'],
+ 50 if wallet is wallets[0] else 0)
+ assert_equal(info['walletname'], wallet_name)
# accessing invalid wallet fails
assert_raises_rpc_error(-18, "Requested wallet does not exist or is not loaded",
wallet_bad.getwalletinfo)
# accessing wallet RPC without using wallet endpoint fails
assert_raises_rpc_error(-19, "Wallet file not specified (must request wallet RPC through /wallet/ uri-path).",
node.getwalletinfo)
- # check w1 wallet balance
- w1_info = w1.getwalletinfo()
- assert_equal(w1_info['immature_balance'], 50)
- w1_name = w1_info['walletname']
- assert_equal(w1_name, "w1")
-
- # check w2 wallet balance
- w2_info = w2.getwalletinfo()
- assert_equal(w2_info['immature_balance'], 0)
- w2_name = w2_info['walletname']
- assert_equal(w2_name, "w2")
-
- w3_name = w3.getwalletinfo()['walletname']
- assert_equal(w3_name, "w3")
-
- w4_name = w4.getwalletinfo()['walletname']
- assert_equal(w4_name, "w")
-
+ w1, w2, w3, w4, *_ = wallets
w1.generate(101)
assert_equal(w1.getbalance(), 100)
assert_equal(w2.getbalance(), 0)
assert_equal(w3.getbalance(), 0)
assert_equal(w4.getbalance(), 0)
w1.sendtoaddress(w2.getnewaddress(), 1)
w1.sendtoaddress(w3.getnewaddress(), 2)
w1.sendtoaddress(w4.getnewaddress(), 3)
w1.generate(1)
assert_equal(w2.getbalance(), 1)
assert_equal(w3.getbalance(), 2)
assert_equal(w4.getbalance(), 3)
batch = w1.batch([w1.getblockchaininfo.get_request(),
w1.getwalletinfo.get_request()])
assert_equal(batch[0]["result"]["chain"], "regtest")
assert_equal(batch[1]["result"]["walletname"], "w1")
if __name__ == '__main__':
MultiWalletTest().main()