Skip to content

Commit be8ab7d

Browse files
committed
Create new wallet databases as directories rather than files
This change should make it easier for users to make complete backups of wallets because they can now just back up the specified `-wallet=<path>` path directly, instead of having to back up the specified path as well as the transaction log directory (for incompletely flushed wallets). Another advantage of this change is that if two wallets are located in the same directory, they will now use their own BerkeleyDB environments instead using a shared environment. Using a shared environment makes it difficult to manage and back up wallets separately because transaction log files will contain a mix of data from all wallets in the environment.
1 parent 26c06f2 commit be8ab7d

File tree

8 files changed

+81
-30
lines changed

8 files changed

+81
-30
lines changed

doc/release-notes.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,31 @@ External wallet files
6767
---------------------
6868

6969
The `-wallet=<path>` option now accepts full paths instead of requiring wallets
70-
to be located in the -walletdir directory. When wallets are located in
71-
different directories, wallet data will be stored independently, so data from
72-
every wallet is not mixed into the same <walletdir>/database/log.??????????
73-
files.
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.
7495

7596
Credits
7697
=======

src/bitcoin-cli.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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/wallet/db.cpp

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,19 @@ std::map<std::string, CDBEnv> g_dbenvs; //!< Map from directory name to open db
5959

6060
CDBEnv* GetWalletEnv(const fs::path& wallet_path, std::string& database_filename)
6161
{
62-
fs::path env_directory = wallet_path.parent_path();
63-
database_filename = wallet_path.filename().string();
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+
}
6475
LOCK(cs_db);
6576
// Note: An ununsed temporary CDBEnv object may be created inside the
6677
// emplace function if the key already exists. This is a little inefficient,

src/wallet/init.cpp

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ std::string GetWalletHelpString(bool showDebug)
3535
strUsage += HelpMessageOpt("-spendzeroconfchange", strprintf(_("Spend unconfirmed change when sending transactions (default: %u)"), DEFAULT_SPEND_ZEROCONF_CHANGE));
3636
strUsage += HelpMessageOpt("-txconfirmtarget=<n>", strprintf(_("If paytxfee is not set, include enough fee so transactions begin confirmation on average within n blocks (default: %u)"), DEFAULT_TX_CONFIRM_TARGET));
3737
strUsage += HelpMessageOpt("-upgradewallet", _("Upgrade wallet to latest format on startup"));
38-
strUsage += HelpMessageOpt("-wallet=<path>", _("Specify wallet database path. Can be specified multiple times to load multiple wallets. Path is interpreted relative to <walletdir> if it is not absolute, and will be created if it does not exist.") + " " + strprintf(_("(default: %s)"), DEFAULT_WALLET_DAT));
38+
strUsage += HelpMessageOpt("-wallet=<path>", _("Specify wallet database path. Can be specified multiple times to load multiple wallets. Path is interpreted relative to <walletdir> if it is not absolute, and will be created if it does not exist (as a directory containing a wallet.dat file and log files). For backwards compatibility this will also accept names of existing data files in <walletdir>.)"));
3939
strUsage += HelpMessageOpt("-walletbroadcast", _("Make the wallet broadcast transactions") + " " + strprintf(_("(default: %u)"), DEFAULT_WALLETBROADCAST));
4040
strUsage += HelpMessageOpt("-walletdir=<dir>", _("Specify directory to hold wallets (default: <datadir>/wallets if it exists, otherwise <datadir>)"));
4141
strUsage += HelpMessageOpt("-walletnotify=<cmd>", _("Execute command when a wallet transaction changes (%s in cmd is replaced by TxID)"));
@@ -66,7 +66,7 @@ bool WalletParameterInteraction()
6666
return true;
6767
}
6868

69-
gArgs.SoftSetArg("-wallet", DEFAULT_WALLET_DAT);
69+
gArgs.SoftSetArg("-wallet", "");
7070
const bool is_multiwallet = gArgs.GetArgs("-wallet").size() > 1;
7171

7272
if (gArgs.GetBoolArg("-blocksonly", DEFAULT_BLOCKSONLY) && gArgs.SoftSetBoolArg("-walletbroadcast", false)) {
@@ -230,10 +230,22 @@ bool VerifyWallets()
230230
std::set<fs::path> wallet_paths;
231231

232232
for (const std::string& walletFile : gArgs.GetArgs("-wallet")) {
233+
// Do some checking on wallet path. It should be either a:
234+
//
235+
// 1. Path where a directory can be created.
236+
// 2. Path to an existing directory.
237+
// 3. Path to a symlink to a directory.
238+
// 4. For backwards compatibility, the name of a data file in -walletdir.
233239
fs::path wallet_path = fs::absolute(walletFile, GetWalletDir());
234-
235-
if (fs::exists(wallet_path) && (!fs::is_regular_file(wallet_path) || fs::is_symlink(wallet_path))) {
236-
return InitError(strprintf(_("Error loading wallet %s. -wallet filename must be a regular file."), walletFile));
240+
fs::file_type path_type = fs::symlink_status(wallet_path).type();
241+
if (!(path_type == fs::file_not_found || path_type == fs::directory_file ||
242+
(path_type == fs::symlink_file && fs::is_directory(wallet_path)) ||
243+
(path_type == fs::regular_file && fs::path(walletFile).filename() == walletFile))) {
244+
return InitError(strprintf(
245+
_("Invalid -wallet path '%s'. -wallet path should point to a directory where wallet.dat and "
246+
"database/log.?????????? files can be stored, a location where such a directory could be created, "
247+
"or (for backwards compatibility) the name of an existing data file in -walletdir (%s)"),
248+
walletFile, GetWalletDir()));
237249
}
238250

239251
if (!wallet_paths.insert(wallet_path).second) {

src/wallet/wallet.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ OutputType g_address_type = OUTPUT_TYPE_NONE;
4545
OutputType g_change_type = OUTPUT_TYPE_NONE;
4646
bool g_wallet_allow_fallback_fee = true; //<! will be defined via chainparams
4747

48-
const char * DEFAULT_WALLET_DAT = "wallet.dat";
4948
const uint32_t BIP32_HARDENED_KEY_LIMIT = 0x80000000;
5049

5150
/**

src/wallet/wallet.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,6 @@ static const bool DEFAULT_WALLET_RBF = false;
6868
static const bool DEFAULT_WALLETBROADCAST = true;
6969
static const bool DEFAULT_DISABLE_WALLET = false;
7070

71-
extern const char * DEFAULT_WALLET_DAT;
72-
7371
static const int64_t TIMESTAMP_MIN = 0;
7472

7573
class CBlockIndex;

test/functional/feature_config_args.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,13 @@ def run_test(self):
3737
os.mkdir(new_data_dir)
3838
self.start_node(0, ['-conf='+conf_file, '-wallet=w1'])
3939
self.stop_node(0)
40-
assert os.path.isfile(os.path.join(new_data_dir, 'regtest', 'wallets', 'w1'))
40+
assert os.path.exists(os.path.join(new_data_dir, 'regtest', 'wallets', 'w1'))
4141

4242
# Ensure command line argument overrides datadir in conf
4343
os.mkdir(new_data_dir_2)
4444
self.nodes[0].datadir = new_data_dir_2
4545
self.start_node(0, ['-datadir='+new_data_dir_2, '-conf='+conf_file, '-wallet=w2'])
46-
assert os.path.isfile(os.path.join(new_data_dir_2, 'regtest', 'wallets', 'w2'))
46+
assert os.path.exists(os.path.join(new_data_dir_2, 'regtest', 'wallets', 'w2'))
4747

4848
if __name__ == '__main__':
4949
ConfArgsTest().main()

test/functional/wallet_multiwallet.py

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,24 +29,38 @@ def run_test(self):
2929
self.stop_nodes()
3030
assert_equal(os.path.isfile(wallet_dir('wallet.dat')), True)
3131

32+
# create symlink to verify wallet directory path can be referenced
33+
# through symlink
34+
os.mkdir(wallet_dir('w7'))
35+
os.symlink('w7', wallet_dir('w7_symlink'))
36+
37+
# rename wallet.dat to make sure plain wallet file paths (as opposed to
38+
# directory paths) can be loaded
39+
os.rename(wallet_dir("wallet.dat"), wallet_dir("w8"))
40+
3241
# restart node with a mix of wallet names:
3342
# w1, w2, w3 - to verify new wallets created when non-existing paths specified
3443
# w - to verify wallet name matching works when one wallet path is prefix of another
3544
# sub/w5 - to verify relative wallet path is created correctly
3645
# extern/w6 - to verify absolute wallet path is created correctly
37-
# wallet.dat - to verify existing wallet file is loaded correctly
38-
wallet_names = ['w1', 'w2', 'w3', 'w', 'sub/w5', os.path.join(self.options.tmpdir, 'extern/w6'), 'wallet.dat']
46+
# w7_symlink - to verify symlinked wallet path is initialized correctly
47+
# w8 - to verify existing wallet file is loaded correctly
48+
# '' - to verify default wallet file is created correctly
49+
wallet_names = ['w1', 'w2', 'w3', 'w', 'sub/w5', os.path.join(self.options.tmpdir, 'extern/w6'), 'w7_symlink', 'w8', '']
3950
extra_args = ['-wallet={}'.format(n) for n in wallet_names]
4051
self.start_node(0, extra_args)
4152
assert_equal(set(node.listwallets()), set(wallet_names))
4253

4354
# check that all requested wallets were created
4455
self.stop_node(0)
4556
for wallet_name in wallet_names:
46-
assert_equal(os.path.isfile(wallet_dir(wallet_name)), True)
57+
if os.path.isdir(wallet_dir(wallet_name)):
58+
assert_equal(os.path.isfile(wallet_dir(wallet_name, "wallet.dat")), True)
59+
else:
60+
assert_equal(os.path.isfile(wallet_dir(wallet_name)), True)
4761

4862
# should not initialize if wallet path can't be created
49-
self.assert_start_raises_init_error(0, ['-wallet=wallet.dat/bad'], 'File exists')
63+
self.assert_start_raises_init_error(0, ['-wallet=wallet.dat/bad'], 'Not a directory')
5064

5165
self.assert_start_raises_init_error(0, ['-walletdir=wallets'], 'Error: Specified -walletdir "wallets" does not exist')
5266
self.assert_start_raises_init_error(0, ['-walletdir=wallets'], 'Error: Specified -walletdir "wallets" is a relative path', cwd=data_dir())
@@ -55,17 +69,13 @@ def run_test(self):
5569
# should not initialize if there are duplicate wallets
5670
self.assert_start_raises_init_error(0, ['-wallet=w1', '-wallet=w1'], 'Error loading wallet w1. Duplicate -wallet filename specified.')
5771

58-
# should not initialize if wallet file is a directory
59-
os.mkdir(wallet_dir('w11'))
60-
self.assert_start_raises_init_error(0, ['-wallet=w11'], 'Error loading wallet w11. -wallet filename must be a regular file.')
61-
6272
# should not initialize if one wallet is a copy of another
63-
shutil.copyfile(wallet_dir('w2'), wallet_dir('w22'))
64-
self.assert_start_raises_init_error(0, ['-wallet=w2', '-wallet=w22'], 'duplicates fileid')
73+
shutil.copyfile(wallet_dir('w8'), wallet_dir('w8_copy'))
74+
self.assert_start_raises_init_error(0, ['-wallet=w8', '-wallet=w8_copy'], 'duplicates fileid')
6575

6676
# should not initialize if wallet file is a symlink
67-
os.symlink(wallet_dir('w1'), wallet_dir('w12'))
68-
self.assert_start_raises_init_error(0, ['-wallet=w12'], 'Error loading wallet w12. -wallet filename must be a regular file.')
77+
os.symlink('w8', wallet_dir('w8_symlink'))
78+
self.assert_start_raises_init_error(0, ['-wallet=w8_symlink'], 'Invalid -wallet path')
6979

7080
# should not initialize if the specified walletdir does not exist
7181
self.assert_start_raises_init_error(0, ['-walletdir=bad'], 'Error: Specified -walletdir "bad" does not exist')

0 commit comments

Comments
 (0)