diff --git a/src/bitcoin-wallet.cpp b/src/bitcoin-wallet.cpp index 87f0837674..0f8d312c5e 100644 --- a/src/bitcoin-wallet.cpp +++ b/src/bitcoin-wallet.cpp @@ -27,15 +27,17 @@ static void SetupWalletToolArgs(ArgsManager& argsman) argsman.AddArg("-version", "Print version and exit", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-datadir=", "Specify data directory", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-wallet=", "Specify wallet name", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::OPTIONS); - argsman.AddArg("-dumpfile=", "When used with 'dump', writes out the records to this file.", ArgsManager::ALLOW_STRING, OptionsCategory::OPTIONS); + argsman.AddArg("-dumpfile=", "When used with 'dump', writes out the records to this file. When used with 'createfromdump', loads the records into a new wallet.", ArgsManager::ALLOW_STRING, OptionsCategory::OPTIONS); argsman.AddArg("-debug=", "Output debugging information (default: 0).", ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST); argsman.AddArg("-descriptors", "Create descriptors wallet. Only for create", ArgsManager::ALLOW_BOOL, OptionsCategory::OPTIONS); + argsman.AddArg("-format=", "The format of the wallet file to create. Either \"bdb\" or \"sqlite\". Only used with 'createfromdump'", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-printtoconsole", "Send trace/debug info to console (default: 1 when no -debug is true, 0 otherwise).", ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST); argsman.AddArg("info", "Get wallet info", ArgsManager::ALLOW_ANY, OptionsCategory::COMMANDS); argsman.AddArg("create", "Create new wallet file", ArgsManager::ALLOW_ANY, OptionsCategory::COMMANDS); argsman.AddArg("salvage", "Attempt to recover private keys from a corrupt wallet. Warning: 'salvage' is experimental.", ArgsManager::ALLOW_ANY, OptionsCategory::COMMANDS); argsman.AddArg("dump", "Print out all of the wallet key-value records", ArgsManager::ALLOW_ANY, OptionsCategory::COMMANDS); + argsman.AddArg("createfromdump", "Create new wallet file from dumped records", ArgsManager::ALLOW_ANY, OptionsCategory::COMMANDS); } static bool WalletAppInit(int argc, char* argv[]) diff --git a/src/wallet/dump.cpp b/src/wallet/dump.cpp index 0d82863de1..e314107988 100644 --- a/src/wallet/dump.cpp +++ b/src/wallet/dump.cpp @@ -94,3 +94,189 @@ bool DumpWallet(CWallet& wallet, bilingual_str& error) return ret; } + +// The standard wallet deleter function blocks on the validation interface +// queue, which doesn't exist for the bitcoin-wallet. Define our own +// deleter here. +static void WalletToolReleaseWallet(CWallet* wallet) +{ + wallet->WalletLogPrintf("Releasing wallet\n"); + wallet->Close(); + delete wallet; +} + +bool CreateFromDump(const std::string& name, const fs::path& wallet_path, bilingual_str& error, std::vector& warnings) +{ + // Get the dumpfile + std::string dump_filename = gArgs.GetArg("-dumpfile", ""); + if (dump_filename.empty()) { + error = _("No dump file provided. To use createfromdump, -dumpfile= must be provided."); + return false; + } + + fs::path dump_path = dump_filename; + dump_path = fs::absolute(dump_path); + if (!fs::exists(dump_path)) { + error = strprintf(_("Dump file %s does not exist."), dump_path.string()); + return false; + } + fsbridge::ifstream dump_file(dump_path); + + // Compute the checksum + CHashWriter hasher(0, 0); + uint256 checksum; + + // Check the magic and version + std::string magic_key; + std::getline(dump_file, magic_key, ','); + std::string version_value; + std::getline(dump_file, version_value, '\n'); + if (magic_key != DUMP_MAGIC) { + error = strprintf(_("Error: Dumpfile identifier record is incorrect. Got \"%s\", expected \"%s\"."), magic_key, DUMP_MAGIC); + dump_file.close(); + return false; + } + // Check the version number (value of first record) + uint32_t ver; + if (!ParseUInt32(version_value, &ver)) { + error =strprintf(_("Error: Unable to parse version %u as a uint32_t"), version_value); + dump_file.close(); + return false; + } + if (ver != DUMP_VERSION) { + error = strprintf(_("Error: Dumpfile version is not supported. This version of bitcoin-wallet only supports version 1 dumpfiles. Got dumpfile with version %s"), version_value); + dump_file.close(); + return false; + } + std::string magic_hasher_line = strprintf("%s,%s\n", magic_key, version_value); + hasher.write(magic_hasher_line.data(), magic_hasher_line.size()); + + // Get the stored file format + std::string format_key; + std::getline(dump_file, format_key, ','); + std::string format_value; + std::getline(dump_file, format_value, '\n'); + if (format_key != "format") { + error = strprintf(_("Error: Dumpfile format record is incorrect. Got \"%s\", expected \"format\"."), format_key); + dump_file.close(); + return false; + } + // Get the data file format with format_value as the default + std::string file_format = gArgs.GetArg("-format", format_value); + if (file_format.empty()) { + error = _("No wallet file format provided. To use createfromdump, -format= must be provided."); + return false; + } + DatabaseFormat data_format; + if (file_format == "bdb") { + data_format = DatabaseFormat::BERKELEY; + } else if (file_format == "sqlite") { + data_format = DatabaseFormat::SQLITE; + } else { + error = strprintf(_("Unknown wallet file format \"%s\" provided. Please provide one of \"bdb\" or \"sqlite\"."), file_format); + return false; + } + if (file_format != format_value) { + warnings.push_back(strprintf(_("Warning: Dumpfile wallet format \"%s\" does not match command line specified format \"%s\"."), format_value, file_format)); + } + std::string format_hasher_line = strprintf("%s,%s\n", format_key, format_value); + hasher.write(format_hasher_line.data(), format_hasher_line.size()); + + DatabaseOptions options; + DatabaseStatus status; + options.require_create = true; + options.require_format = data_format; + std::unique_ptr database = MakeDatabase(wallet_path, options, status, error); + if (!database) return false; + + // dummy chain interface + bool ret = true; + std::shared_ptr wallet(new CWallet(nullptr /* chain */, name, std::move(database)), WalletToolReleaseWallet); + { + LOCK(wallet->cs_wallet); + bool first_run = true; + DBErrors load_wallet_ret = wallet->LoadWallet(first_run); + if (load_wallet_ret != DBErrors::LOAD_OK) { + error = strprintf(_("Error creating %s"), name); + return false; + } + + // Get the database handle + WalletDatabase& db = wallet->GetDatabase(); + std::unique_ptr batch = db.MakeBatch(); + batch->TxnBegin(); + + // Read the records from the dump file and write them to the database + while (dump_file.good()) { + std::string key; + std::getline(dump_file, key, ','); + std::string value; + std::getline(dump_file, value, '\n'); + + if (key == "checksum") { + std::vector parsed_checksum = ParseHex(value); + std::copy(parsed_checksum.begin(), parsed_checksum.end(), checksum.begin()); + break; + } + + std::string line = strprintf("%s,%s\n", key, value); + hasher.write(line.data(), line.size()); + + if (key.empty() || value.empty()) { + continue; + } + + if (!IsHex(key)) { + error = strprintf(_("Error: Got key that was not hex: %s"), key); + ret = false; + break; + } + if (!IsHex(value)) { + error = strprintf(_("Error: Got value that was not hex: %s"), value); + ret = false; + break; + } + + std::vector k = ParseHex(key); + std::vector v = ParseHex(value); + + CDataStream ss_key(k, SER_DISK, CLIENT_VERSION); + CDataStream ss_value(v, SER_DISK, CLIENT_VERSION); + + if (!batch->Write(ss_key, ss_value)) { + error = strprintf(_("Error: Unable to write record to new wallet")); + ret = false; + break; + } + } + + if (ret) { + uint256 comp_checksum = hasher.GetHash(); + if (checksum.IsNull()) { + error = _("Error: Missing checksum"); + ret = false; + } else if (checksum != comp_checksum) { + error = strprintf(_("Error: Dumpfile checksum does not match. Computed %s, expected %s"), HexStr(comp_checksum), HexStr(checksum)); + ret = false; + } + } + + if (ret) { + batch->TxnCommit(); + } else { + batch->TxnAbort(); + } + + batch.reset(); + + dump_file.close(); + } + wallet.reset(); // The pointer deleter will close the wallet for us. + + // Remove the wallet dir if we have a failure + if (!ret) { + fs::remove_all(wallet_path); + } + + return ret; +} diff --git a/src/wallet/dump.h b/src/wallet/dump.h index 0f17ee1d0d..d0a4f5ef1d 100644 --- a/src/wallet/dump.h +++ b/src/wallet/dump.h @@ -5,10 +5,13 @@ #ifndef BITCOIN_WALLET_DUMP_H #define BITCOIN_WALLET_DUMP_H +#include + class CWallet; struct bilingual_str; bool DumpWallet(CWallet& wallet, bilingual_str& error); +bool CreateFromDump(const std::string& name, const fs::path& wallet_path, bilingual_str& error, std::vector& warnings); #endif // BITCOIN_WALLET_DUMP_H diff --git a/src/wallet/wallettool.cpp b/src/wallet/wallettool.cpp index 39dad87184..fe3fcb32c2 100644 --- a/src/wallet/wallettool.cpp +++ b/src/wallet/wallettool.cpp @@ -107,6 +107,11 @@ bool ExecuteWalletToolFunc(const std::string& command, const std::string& name) { fs::path path = fs::absolute(name, GetWalletDir()); + // -format is only allowed with createfromdump. Disallow it for all other commands. + if (gArgs.IsArgSet("-format") && command != "createfromdump") { + tfm::format(std::cerr, "The -format option can only be used with the \"createfromdump\" command.\n"); + return false; + } // -dumpfile is only allowed with dump and createfromdump. Disallow it for all other commands. if (gArgs.IsArgSet("-dumpfile") && command != "dump" && command != "createfromdump") { tfm::format(std::cerr, "The -dumpfile option can only be used with the \"dump\" and \"createfromdump\" commands.\n"); @@ -164,6 +169,17 @@ bool ExecuteWalletToolFunc(const std::string& command, const std::string& name) } tfm::format(std::cout, "The dumpfile may contain private keys. To ensure the safety of your Bitcoin, do not share the dumpfile.\n"); return ret; + } else if (command == "createfromdump") { + bilingual_str error; + std::vector warnings; + bool ret = CreateFromDump(name, path, error, warnings); + for (const auto& warning : warnings) { + tfm::format(std::cout, "%s\n", warning.original); + } + if (!ret && !error.empty()) { + tfm::format(std::cerr, "%s\n", error.original); + } + return ret; } else { tfm::format(std::cerr, "Invalid command: %s\n", command); return false;