Skip to content

Commit 98bc27f

Browse files
committed
Merge #11687: External wallet files
be8ab7d Create new wallet databases as directories rather than files (Russell Yanofsky) 26c06f2 Allow wallet files not in -walletdir directory (Russell Yanofsky) d8a99f6 Allow wallet files in multiple directories (Russell Yanofsky) Pull request description: This change consists of three commits: * The first commit is a pure refactoring that removes the restriction that two wallets can only be opened at the same time if they are contained in the same directory. * The second commit removes the restriction that `-wallet` filenames can only refer to files in the `-walletdir` directory. * The third commit makes second commit a little safer by changing bitcoin to create wallet databases as directories rather than files, so they can be safely backed up. All three commits should be straightforward: * The first commit adds around 20 lines of new code and then updates a bunch of function signatures (generally updating them to take plain fs::path parameters, instead of combinations of strings, fs::paths, and objects like CDBEnv and CWalletDBWrapper). * The second commit removes two `-wallet` filename checks and adds some test cases to the multiwallet unit test. * The third commit just changes the mapping from specified wallet paths to bdb environment & data paths. --- **Note:** For anybody looking at this PR for the first time, I think you can skip the comments before _20 Nov_ and start reading at bitcoin/bitcoin#11687 (comment). Comments before _20 Nov_ were about an earlier version of the PR that didn't include the third commit, and then confusion from not seeing the first commit. Tree-SHA512: 00bbb120fe0df847cf57014f75f1f7f1f58b0b62fa0b3adab4560163ebdfe06ccdfff33b4231693f03c5dc23601cb41954a07bcea9a4919c8d42f7d62bcf6024
2 parents 8a43bdc + be8ab7d commit 98bc27f

17 files changed

+313
-207
lines changed

doc/release-notes.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,36 @@ RPC changes
6363

6464
- The `fundrawtransaction` rpc will reject the previously deprecated `reserveChangeKey` option.
6565

66+
External wallet files
67+
---------------------
68+
69+
The `-wallet=<path>` option now accepts full paths instead of requiring wallets
70+
to be located in the -walletdir directory.
71+
72+
Newly created wallet format
73+
---------------------------
74+
75+
If `-wallet=<path>` is specified with a path that does not exist, it will now
76+
create a wallet directory at the specified location (containing a wallet.dat
77+
data file, a db.log file, and database/log.?????????? files) instead of just
78+
creating a data file at the path and storing log files in the parent
79+
directory. This should make backing up wallets more straightforward than
80+
before because the specified wallet path can just be directly archived without
81+
having to look in the parent directory for transaction log files.
82+
83+
For backwards compatibility, wallet paths that are names of existing data files
84+
in the `-walletdir` directory will continue to be accepted and interpreted the
85+
same as before.
86+
87+
Low-level RPC changes
88+
---------------------
89+
90+
- When bitcoin is not started with any `-wallet=<path>` options, the name of
91+
the default wallet returned by `getwalletinfo` and `listwallets` RPCs is
92+
now the empty string `""` instead of `"wallet.dat"`. If bitcoin is started
93+
with any `-wallet=<path>` options, there is no change in behavior, and the
94+
name of any wallet is just its `<path>` string.
95+
6696
Credits
6797
=======
6898

src/bench/coin_selection.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ static void addCoin(const CAmount& nValue, const CWallet& wallet, std::vector<CO
3232
// (https://github.com/bitcoin/bitcoin/issues/7883#issuecomment-224807484)
3333
static void CoinSelection(benchmark::State& state)
3434
{
35-
const CWallet wallet;
35+
const CWallet wallet("dummy", CWalletDBWrapper::CreateDummy());
3636
std::vector<COutput> vCoins;
3737
LOCK(wallet.cs_wallet);
3838

src/bitcoin-cli.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ std::string HelpMessageCli()
4646
strUsage += HelpMessageOpt("-rpcport=<port>", strprintf(_("Connect to JSON-RPC on <port> (default: %u or testnet: %u)"), defaultBaseParams->RPCPort(), testnetBaseParams->RPCPort()));
4747
strUsage += HelpMessageOpt("-rpcuser=<user>", _("Username for JSON-RPC connections"));
4848
strUsage += HelpMessageOpt("-rpcwait", _("Wait for RPC server to start"));
49-
strUsage += HelpMessageOpt("-rpcwallet=<walletname>", _("Send RPC for non-default wallet on RPC server (argument is wallet filename in bitcoind directory, required if bitcoind/-Qt runs with multiple wallets)"));
49+
strUsage += HelpMessageOpt("-rpcwallet=<walletname>", _("Send RPC for non-default wallet on RPC server (needs to exactly match corresponding -wallet option passed to bitcoind)"));
5050
strUsage += HelpMessageOpt("-stdin", _("Read extra arguments from standard input, one per line until EOF/Ctrl-D (recommended for sensitive information such as passphrases). When combined with -stdinrpcpass, the first line from standard input is used for the RPC password."));
5151
strUsage += HelpMessageOpt("-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.")));
5252

@@ -339,8 +339,8 @@ static UniValue CallRPC(BaseRequestHandler *rh, const std::string& strMethod, co
339339

340340
// check if we should use a special wallet endpoint
341341
std::string endpoint = "/";
342-
std::string walletName = gArgs.GetArg("-rpcwallet", "");
343-
if (!walletName.empty()) {
342+
if (!gArgs.GetArgs("-rpcwallet").empty()) {
343+
std::string walletName = gArgs.GetArg("-rpcwallet", "");
344344
char *encodedURI = evhttp_uriencode(walletName.c_str(), walletName.size(), false);
345345
if (encodedURI) {
346346
endpoint = "/wallet/"+ std::string(encodedURI);

src/qt/test/wallettests.cpp

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,7 @@ void TestGUI()
158158
for (int i = 0; i < 5; ++i) {
159159
test.CreateAndProcessBlock({}, GetScriptForRawPubKey(test.coinbaseKey.GetPubKey()));
160160
}
161-
bitdb.MakeMock();
162-
std::unique_ptr<CWalletDBWrapper> dbw(new CWalletDBWrapper(&bitdb, "wallet_test.dat"));
163-
CWallet wallet(std::move(dbw));
161+
CWallet wallet("mock", CWalletDBWrapper::CreateMock());
164162
bool firstRun;
165163
wallet.LoadWallet(firstRun);
166164
{
@@ -261,9 +259,6 @@ void TestGUI()
261259
QPushButton* removeRequestButton = receiveCoinsDialog.findChild<QPushButton*>("removeRequestButton");
262260
removeRequestButton->click();
263261
QCOMPARE(requestTableModel->rowCount({}), currentRowCount-1);
264-
265-
bitdb.Flush(true);
266-
bitdb.Reset();
267262
}
268263

269264
}

src/wallet/db.cpp

Lines changed: 90 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -52,20 +52,55 @@ void CheckUniqueFileid(const CDBEnv& env, const std::string& filename, Db& db)
5252
}
5353
}
5454
}
55+
56+
CCriticalSection cs_db;
57+
std::map<std::string, CDBEnv> g_dbenvs; //!< Map from directory name to open db environment.
5558
} // namespace
5659

60+
CDBEnv* GetWalletEnv(const fs::path& wallet_path, std::string& database_filename)
61+
{
62+
fs::path env_directory;
63+
if (fs::is_regular_file(wallet_path)) {
64+
// Special case for backwards compatibility: if wallet path points to an
65+
// existing file, treat it as the path to a BDB data file in a parent
66+
// directory that also contains BDB log files.
67+
env_directory = wallet_path.parent_path();
68+
database_filename = wallet_path.filename().string();
69+
} else {
70+
// Normal case: Interpret wallet path as a directory path containing
71+
// data and log files.
72+
env_directory = wallet_path;
73+
database_filename = "wallet.dat";
74+
}
75+
LOCK(cs_db);
76+
// Note: An ununsed temporary CDBEnv object may be created inside the
77+
// emplace function if the key already exists. This is a little inefficient,
78+
// but not a big concern since the map will be changed in the future to hold
79+
// pointers instead of objects, anyway.
80+
return &g_dbenvs.emplace(std::piecewise_construct, std::forward_as_tuple(env_directory.string()), std::forward_as_tuple(env_directory)).first->second;
81+
}
82+
5783
//
5884
// CDB
5985
//
6086

61-
CDBEnv bitdb;
62-
63-
void CDBEnv::EnvShutdown()
87+
void CDBEnv::Close()
6488
{
6589
if (!fDbEnvInit)
6690
return;
6791

6892
fDbEnvInit = false;
93+
94+
for (auto& db : mapDb) {
95+
auto count = mapFileUseCount.find(db.first);
96+
assert(count == mapFileUseCount.end() || count->second == 0);
97+
if (db.second) {
98+
db.second->close(0);
99+
delete db.second;
100+
db.second = nullptr;
101+
}
102+
}
103+
69104
int ret = dbenv->close(0);
70105
if (ret != 0)
71106
LogPrintf("CDBEnv::EnvShutdown: Error %d shutting down database environment: %s\n", ret, DbEnv::strerror(ret));
@@ -80,29 +115,25 @@ void CDBEnv::Reset()
80115
fMockDb = false;
81116
}
82117

83-
CDBEnv::CDBEnv()
118+
CDBEnv::CDBEnv(const fs::path& dir_path) : strPath(dir_path.string())
84119
{
85120
Reset();
86121
}
87122

88123
CDBEnv::~CDBEnv()
89124
{
90-
EnvShutdown();
125+
Close();
91126
}
92127

93-
void CDBEnv::Close()
94-
{
95-
EnvShutdown();
96-
}
97-
98-
bool CDBEnv::Open(const fs::path& pathIn, bool retry)
128+
bool CDBEnv::Open(bool retry)
99129
{
100130
if (fDbEnvInit)
101131
return true;
102132

103133
boost::this_thread::interruption_point();
104134

105-
strPath = pathIn.string();
135+
fs::path pathIn = strPath;
136+
TryCreateDirectories(pathIn);
106137
if (!LockDirectory(pathIn, ".walletlock")) {
107138
LogPrintf("Cannot obtain a lock on wallet directory %s. Another instance of bitcoin may be using it.\n", strPath);
108139
return false;
@@ -150,7 +181,7 @@ bool CDBEnv::Open(const fs::path& pathIn, bool retry)
150181
// failure is ok (well, not really, but it's not worse than what we started with)
151182
}
152183
// try opening it again one more time
153-
if (!Open(pathIn, false)) {
184+
if (!Open(false /* retry */)) {
154185
// if it still fails, it probably means we can't even create the database env
155186
return false;
156187
}
@@ -209,12 +240,15 @@ CDBEnv::VerifyResult CDBEnv::Verify(const std::string& strFile, recoverFunc_type
209240
return RECOVER_FAIL;
210241

211242
// Try to recover:
212-
bool fRecovered = (*recoverFunc)(strFile, out_backup_filename);
243+
bool fRecovered = (*recoverFunc)(fs::path(strPath) / strFile, out_backup_filename);
213244
return (fRecovered ? RECOVER_OK : RECOVER_FAIL);
214245
}
215246

216-
bool CDB::Recover(const std::string& filename, void *callbackDataIn, bool (*recoverKVcallback)(void* callbackData, CDataStream ssKey, CDataStream ssValue), std::string& newFilename)
247+
bool CDB::Recover(const fs::path& file_path, void *callbackDataIn, bool (*recoverKVcallback)(void* callbackData, CDataStream ssKey, CDataStream ssValue), std::string& newFilename)
217248
{
249+
std::string filename;
250+
CDBEnv* env = GetWalletEnv(file_path, filename);
251+
218252
// Recovery procedure:
219253
// move wallet file to walletfilename.timestamp.bak
220254
// Call Salvage with fAggressive=true to
@@ -225,7 +259,7 @@ bool CDB::Recover(const std::string& filename, void *callbackDataIn, bool (*reco
225259
int64_t now = GetTime();
226260
newFilename = strprintf("%s.%d.bak", filename, now);
227261

228-
int result = bitdb.dbenv->dbrename(nullptr, filename.c_str(), nullptr,
262+
int result = env->dbenv->dbrename(nullptr, filename.c_str(), nullptr,
229263
newFilename.c_str(), DB_AUTO_COMMIT);
230264
if (result == 0)
231265
LogPrintf("Renamed %s to %s\n", filename, newFilename);
@@ -236,15 +270,15 @@ bool CDB::Recover(const std::string& filename, void *callbackDataIn, bool (*reco
236270
}
237271

238272
std::vector<CDBEnv::KeyValPair> salvagedData;
239-
bool fSuccess = bitdb.Salvage(newFilename, true, salvagedData);
273+
bool fSuccess = env->Salvage(newFilename, true, salvagedData);
240274
if (salvagedData.empty())
241275
{
242276
LogPrintf("Salvage(aggressive) found no records in %s.\n", newFilename);
243277
return false;
244278
}
245279
LogPrintf("Salvage(aggressive) found %u records\n", salvagedData.size());
246280

247-
std::unique_ptr<Db> pdbCopy = MakeUnique<Db>(bitdb.dbenv.get(), 0);
281+
std::unique_ptr<Db> pdbCopy = MakeUnique<Db>(env->dbenv.get(), 0);
248282
int ret = pdbCopy->open(nullptr, // Txn pointer
249283
filename.c_str(), // Filename
250284
"main", // Logical db name
@@ -257,7 +291,7 @@ bool CDB::Recover(const std::string& filename, void *callbackDataIn, bool (*reco
257291
return false;
258292
}
259293

260-
DbTxn* ptxn = bitdb.TxnBegin();
294+
DbTxn* ptxn = env->TxnBegin();
261295
for (CDBEnv::KeyValPair& row : salvagedData)
262296
{
263297
if (recoverKVcallback)
@@ -279,8 +313,12 @@ bool CDB::Recover(const std::string& filename, void *callbackDataIn, bool (*reco
279313
return fSuccess;
280314
}
281315

282-
bool CDB::VerifyEnvironment(const std::string& walletFile, const fs::path& walletDir, std::string& errorStr)
316+
bool CDB::VerifyEnvironment(const fs::path& file_path, std::string& errorStr)
283317
{
318+
std::string walletFile;
319+
CDBEnv* env = GetWalletEnv(file_path, walletFile);
320+
fs::path walletDir = env->Directory();
321+
284322
LogPrintf("Using BerkeleyDB version %s\n", DbEnv::version(0, 0, 0));
285323
LogPrintf("Using wallet %s\n", walletFile);
286324

@@ -291,20 +329,24 @@ bool CDB::VerifyEnvironment(const std::string& walletFile, const fs::path& walle
291329
return false;
292330
}
293331

294-
if (!bitdb.Open(walletDir, true)) {
332+
if (!env->Open(true /* retry */)) {
295333
errorStr = strprintf(_("Error initializing wallet database environment %s!"), walletDir);
296334
return false;
297335
}
298336

299337
return true;
300338
}
301339

302-
bool CDB::VerifyDatabaseFile(const std::string& walletFile, const fs::path& walletDir, std::string& warningStr, std::string& errorStr, CDBEnv::recoverFunc_type recoverFunc)
340+
bool CDB::VerifyDatabaseFile(const fs::path& file_path, std::string& warningStr, std::string& errorStr, CDBEnv::recoverFunc_type recoverFunc)
303341
{
342+
std::string walletFile;
343+
CDBEnv* env = GetWalletEnv(file_path, walletFile);
344+
fs::path walletDir = env->Directory();
345+
304346
if (fs::exists(walletDir / walletFile))
305347
{
306348
std::string backup_filename;
307-
CDBEnv::VerifyResult r = bitdb.Verify(walletFile, recoverFunc, backup_filename);
349+
CDBEnv::VerifyResult r = env->Verify(walletFile, recoverFunc, backup_filename);
308350
if (r == CDBEnv::RECOVER_OK)
309351
{
310352
warningStr = strprintf(_("Warning: Wallet file corrupt, data salvaged!"
@@ -414,8 +456,8 @@ CDB::CDB(CWalletDBWrapper& dbw, const char* pszMode, bool fFlushOnCloseIn) : pdb
414456
nFlags |= DB_CREATE;
415457

416458
{
417-
LOCK(env->cs_db);
418-
if (!env->Open(GetWalletDir()))
459+
LOCK(cs_db);
460+
if (!env->Open(false /* retry */))
419461
throw std::runtime_error("CDB: Failed to open database environment.");
420462

421463
pdb = env->mapDb[strFilename];
@@ -442,7 +484,25 @@ CDB::CDB(CWalletDBWrapper& dbw, const char* pszMode, bool fFlushOnCloseIn) : pdb
442484
if (ret != 0) {
443485
throw std::runtime_error(strprintf("CDB: Error %d, can't open database %s", ret, strFilename));
444486
}
445-
CheckUniqueFileid(*env, strFilename, *pdb_temp);
487+
488+
// Call CheckUniqueFileid on the containing BDB environment to
489+
// avoid BDB data consistency bugs that happen when different data
490+
// files in the same environment have the same fileid.
491+
//
492+
// Also call CheckUniqueFileid on all the other g_dbenvs to prevent
493+
// bitcoin from opening the same data file through another
494+
// environment when the file is referenced through equivalent but
495+
// not obviously identical symlinked or hard linked or bind mounted
496+
// paths. In the future a more relaxed check for equal inode and
497+
// device ids could be done instead, which would allow opening
498+
// different backup copies of a wallet at the same time. Maybe even
499+
// more ideally, an exclusive lock for accessing the database could
500+
// be implemented, so no equality checks are needed at all. (Newer
501+
// versions of BDB have an set_lk_exclusive method for this
502+
// purpose, but the older version we use does not.)
503+
for (auto& env : g_dbenvs) {
504+
CheckUniqueFileid(env.second, strFilename, *pdb_temp);
505+
}
446506

447507
pdb = pdb_temp.release();
448508
env->mapDb[strFilename] = pdb;
@@ -490,7 +550,7 @@ void CDB::Close()
490550
Flush();
491551

492552
{
493-
LOCK(env->cs_db);
553+
LOCK(cs_db);
494554
--env->mapFileUseCount[strFile];
495555
}
496556
}
@@ -518,7 +578,7 @@ bool CDB::Rewrite(CWalletDBWrapper& dbw, const char* pszSkip)
518578
const std::string& strFile = dbw.strFile;
519579
while (true) {
520580
{
521-
LOCK(env->cs_db);
581+
LOCK(cs_db);
522582
if (!env->mapFileUseCount.count(strFile) || env->mapFileUseCount[strFile] == 0) {
523583
// Flush log data to the dat file
524584
env->CloseDb(strFile);
@@ -646,7 +706,7 @@ bool CDB::PeriodicFlush(CWalletDBWrapper& dbw)
646706
bool ret = false;
647707
CDBEnv *env = dbw.env;
648708
const std::string& strFile = dbw.strFile;
649-
TRY_LOCK(bitdb.cs_db,lockDb);
709+
TRY_LOCK(cs_db, lockDb);
650710
if (lockDb)
651711
{
652712
// Don't do this if any databases are in use
@@ -694,7 +754,7 @@ bool CWalletDBWrapper::Backup(const std::string& strDest)
694754
while (true)
695755
{
696756
{
697-
LOCK(env->cs_db);
757+
LOCK(cs_db);
698758
if (!env->mapFileUseCount.count(strFile) || env->mapFileUseCount[strFile] == 0)
699759
{
700760
// Flush log data to the dat file

0 commit comments

Comments
 (0)