diff --git a/ci/test/00_setup_env_native_cxx20.sh b/ci/test/00_setup_env_native_cxx20.sh index 24a1195d6b..422da6b4ca 100755 --- a/ci/test/00_setup_env_native_cxx20.sh +++ b/ci/test/00_setup_env_native_cxx20.sh @@ -9,7 +9,7 @@ export LC_ALL=C.UTF-8 export CONTAINER_NAME=ci_native_cxx20 export PACKAGES="python3-zmq qtbase5-dev qttools5-dev-tools libdbus-1-dev libharfbuzz-dev" export DEP_OPTS="NO_UPNP=1 DEBUG=1" -export CPPFLAGS="-DDEBUG_LOCKORDER -DENABLE_DASH_DEBUG -DARENA_DEBUG" +export CPPFLAGS="-DDEBUG_LOCKORDER -DARENA_DEBUG" export PYZMQ=true export RUN_FUNCTIONAL_TESTS=false export GOAL="install" diff --git a/ci/test/00_setup_env_native_fuzz.sh b/ci/test/00_setup_env_native_fuzz.sh index d7cad6126e..a6f7f629a8 100755 --- a/ci/test/00_setup_env_native_fuzz.sh +++ b/ci/test/00_setup_env_native_fuzz.sh @@ -9,7 +9,7 @@ export LC_ALL=C.UTF-8 export CONTAINER_NAME=ci_native_fuzz export PACKAGES="clang llvm python3 libevent-dev bsdmainutils libboost-filesystem-dev libboost-test-dev" export DEP_OPTS="NO_UPNP=1 DEBUG=1" -export CPPFLAGS="-DDEBUG_LOCKORDER -DENABLE_DASH_DEBUG -DARENA_DEBUG" +export CPPFLAGS="-DDEBUG_LOCKORDER -DARENA_DEBUG" export CXXFLAGS="-Werror -Wno-unused-command-line-argument -Wno-unused-value -Wno-deprecated-builtins" export PYZMQ=true export RUN_UNIT_TESTS=false diff --git a/ci/test/00_setup_env_native_tsan.sh b/ci/test/00_setup_env_native_tsan.sh index bcdc05103c..633304580f 100755 --- a/ci/test/00_setup_env_native_tsan.sh +++ b/ci/test/00_setup_env_native_tsan.sh @@ -13,5 +13,5 @@ export TEST_RUNNER_EXTRA="--extended --exclude feature_pruning,feature_dbcrash,w export TEST_RUNNER_EXTRA="${TEST_RUNNER_EXTRA} --timeout-factor=4" # Increase timeout because sanitizers slow down export GOAL="install" export BITCOIN_CONFIG="--enable-zmq --with-gui=no --with-sanitizers=thread CC=clang-16 CXX=clang++-16 CXXFLAGS='-g' --with-boost-process" -export CPPFLAGS="-DDEBUG_LOCKORDER -DENABLE_DASH_DEBUG -DARENA_DEBUG" +export CPPFLAGS="-DDEBUG_LOCKORDER -DARENA_DEBUG" export PYZMQ=true diff --git a/src/Makefile.am b/src/Makefile.am index c81ffde07f..7d651fbb1b 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -371,6 +371,7 @@ BITCOIN_CORE_H = \ wallet/context.h \ wallet/crypter.h \ wallet/db.h \ + wallet/dump.h \ wallet/fees.h \ wallet/hdchain.h \ wallet/ismine.h \ @@ -549,6 +550,7 @@ libbitcoin_wallet_a_SOURCES = \ wallet/context.cpp \ wallet/crypter.cpp \ wallet/db.cpp \ + wallet/dump.cpp \ wallet/fees.cpp \ wallet/hdchain.cpp \ wallet/interfaces.cpp \ diff --git a/src/bitcoin-wallet.cpp b/src/bitcoin-wallet.cpp index dce030695b..e4e9247a78 100644 --- a/src/bitcoin-wallet.cpp +++ b/src/bitcoin-wallet.cpp @@ -25,9 +25,13 @@ static void SetupWalletToolArgs(ArgsManager& argsman) SetupChainParamsBaseOptions(argsman); argsman.AddArg("-version", "Print version and exit", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); + argsman.AddArg("-usehd", strprintf("Create HD (hierarchical deterministic) wallet (default: %d)", true), 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, 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); @@ -36,6 +40,8 @@ static void SetupWalletToolArgs(ArgsManager& argsman) // Hidden argsman.AddArg("salvage", "Attempt to recover private keys from a corrupt wallet. Warning: 'salvage' is experimental.", ArgsManager::ALLOW_ANY, OptionsCategory::COMMANDS); argsman.AddArg("wipetxes", "Wipe all transactions from a wallet", 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[]) @@ -114,8 +120,9 @@ MAIN_FUNCTION std::string name = gArgs.GetArg("-wallet", ""); ECC_Start(); - if (!WalletTool::ExecuteWalletToolFunc(method, name)) + if (!WalletTool::ExecuteWalletToolFunc(gArgs, method, name)) { return EXIT_FAILURE; + } ECC_Stop(); return EXIT_SUCCESS; } diff --git a/src/util/system.h b/src/util/system.h index f00ff20031..3aa787d766 100644 --- a/src/util/system.h +++ b/src/util/system.h @@ -34,17 +34,6 @@ #include #include -// Debugging macros - -// Uncomment the following line to enable debugging messages -// or enable on a per file basis prior to inclusion of util.h -//#define ENABLE_DASH_DEBUG -#ifdef ENABLE_DASH_DEBUG -#define DBG( x ) x -#else -#define DBG( x ) -#endif - //Dash only features extern bool fMasternodeMode; diff --git a/src/wallet/dump.cpp b/src/wallet/dump.cpp new file mode 100644 index 0000000000..b5e735b084 --- /dev/null +++ b/src/wallet/dump.cpp @@ -0,0 +1,282 @@ +// Copyright (c) 2020 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 + +static const std::string DUMP_MAGIC = "BITCOIN_CORE_WALLET_DUMP"; +uint32_t DUMP_VERSION = 1; + +bool DumpWallet(CWallet& wallet, bilingual_str& error) +{ + // Get the dumpfile + std::string dump_filename = gArgs.GetArg("-dumpfile", ""); + if (dump_filename.empty()) { + error = _("No dump file provided. To use dump, -dumpfile= must be provided."); + return false; + } + + fs::path path = dump_filename; + path = fs::absolute(path); + if (fs::exists(path)) { + error = strprintf(_("File %s already exists. If you are sure this is what you want, move it out of the way first."), path.string()); + return false; + } + fsbridge::ofstream dump_file; + dump_file.open(path); + if (dump_file.fail()) { + error = strprintf(_("Unable to open %s for writing"), path.string()); + return false; + } + + CHashWriter hasher(0, 0); + + WalletDatabase& db = wallet.GetDatabase(); + std::unique_ptr batch = db.MakeBatch(); + + bool ret = true; + if (!batch->StartCursor()) { + error = _("Error: Couldn't create cursor into database"); + ret = false; + } + + // Write out a magic string with version + std::string line = strprintf("%s,%u\n", DUMP_MAGIC, DUMP_VERSION); + dump_file.write(line.data(), line.size()); + hasher << Span{line}; + + // Write out the file format + line = strprintf("%s,%s\n", "format", db.Format()); + dump_file.write(line.data(), line.size()); + hasher << Span{line}; + + if (ret) { + + // Read the records + while (true) { + CDataStream ss_key(SER_DISK, CLIENT_VERSION); + CDataStream ss_value(SER_DISK, CLIENT_VERSION); + bool complete; + ret = batch->ReadAtCursor(ss_key, ss_value, complete); + if (complete) { + ret = true; + break; + } else if (!ret) { + error = _("Error reading next record from wallet database"); + break; + } + std::string key_str = HexStr(ss_key); + std::string value_str = HexStr(ss_value); + line = strprintf("%s,%s\n", key_str, value_str); + dump_file.write(line.data(), line.size()); + hasher << Span{line}; + } + } + + batch->CloseCursor(); + batch.reset(); + + // Close the wallet after we're done with it. The caller won't be doing this + wallet.Close(); + + if (ret) { + // Write the hash + tfm::format(dump_file, "checksum,%s\n", HexStr(hasher.GetHash())); + dump_file.close(); + } else { + // Remove the dumpfile on failure + dump_file.close(); + fs::remove(path); + } + + 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 << Span{magic_hasher_line}; + + // 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 << Span{format_hasher_line}; + + 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 */, /*coinjoin_loader=*/ nullptr, 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 << Span{line}; + + 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 new file mode 100644 index 0000000000..d0a4f5ef1d --- /dev/null +++ b/src/wallet/dump.h @@ -0,0 +1,17 @@ +// Copyright (c) 2020 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#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/hdchain.cpp b/src/wallet/hdchain.cpp index b4fca803a3..516b8c38ba 100644 --- a/src/wallet/hdchain.cpp +++ b/src/wallet/hdchain.cpp @@ -8,7 +8,6 @@ #include #include #include -#include bool CHDChain::SetNull() { @@ -41,33 +40,6 @@ bool CHDChain::IsCrypted() const return fCrypted; } -void CHDChain::Debug(const std::string& strName) const -{ - DBG( - LOCK(cs); - std::cout << __func__ << ": ---" << strName << "---" << std::endl; - if (fCrypted) { - std::cout << "mnemonic: ***CRYPTED***" << std::endl; - std::cout << "mnemonicpassphrase: ***CRYPTED***" << std::endl; - std::cout << "seed: ***CRYPTED***" << std::endl; - } else { - std::cout << "mnemonic: " << std::string(vchMnemonic.begin(), vchMnemonic.end()).c_str() << std::endl; - std::cout << "mnemonicpassphrase: " << std::string(vchMnemonicPassphrase.begin(), vchMnemonicPassphrase.end()).c_str() << std::endl; - std::cout << "seed: " << HexStr(vchSeed).c_str() << std::endl; - - CExtKey extkey; - extkey.SetSeed(vchSeed); - - std::cout << "extended private masterkey: " << EncodeExtKey(extkey).c_str() << std::endl; - - CExtPubKey extpubkey; - extpubkey = extkey.Neuter(); - - std::cout << "extended public masterkey: " << EncodeExtPubKey(extpubkey).c_str() << std::endl; - } - ); -} - bool CHDChain::SetMnemonic(const SecureVector& vchMnemonic, const SecureVector& vchMnemonicPassphrase, bool fUpdateID) { return SetMnemonic(SecureString(vchMnemonic.begin(), vchMnemonic.end()), SecureString(vchMnemonicPassphrase.begin(), vchMnemonicPassphrase.end()), fUpdateID); diff --git a/src/wallet/hdchain.h b/src/wallet/hdchain.h index bacb834ddc..6d2b6cccfe 100644 --- a/src/wallet/hdchain.h +++ b/src/wallet/hdchain.h @@ -98,8 +98,6 @@ public: void SetCrypted(bool fCryptedIn); bool IsCrypted() const; - void Debug(const std::string& strName) const; - bool SetMnemonic(const SecureVector& vchMnemonic, const SecureVector& vchMnemonicPassphrase, bool fUpdateID); bool SetMnemonic(const SecureString& ssMnemonic, const SecureString& ssMnemonicPassphrase, bool fUpdateID); bool GetMnemonic(SecureVector& vchMnemonicRet, SecureVector& vchMnemonicPassphraseRet) const; diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp index 2c9a3c0034..7021967601 100644 --- a/src/wallet/scriptpubkeyman.cpp +++ b/src/wallet/scriptpubkeyman.cpp @@ -273,11 +273,6 @@ bool LegacyScriptPubKeyMan::Encrypt(const CKeyingMaterial& master_key, WalletBat CHDChain hdChainCrypted; assert(GetHDChain(hdChainCrypted)); - DBG( - tfm::format(std::cout, "EncryptWallet -- current seed: '%s'\n", HexStr(hdChainCurrent.GetSeed())); - tfm::format(std::cout, "EncryptWallet -- crypted seed: '%s'\n", HexStr(hdChainCrypted.GetSeed())); - ); - // ids should match, seed hashes should not assert(hdChainCurrent.GetID() == hdChainCrypted.GetID()); assert(hdChainCurrent.GetSeedHash() != hdChainCrypted.GetSeedHash()); @@ -396,7 +391,6 @@ void LegacyScriptPubKeyMan::GenerateNewCryptedHDChain(const SecureString& secure // add default account hdChainTmp.AddAccount(); - hdChainTmp.Debug(__func__); // We need to safe chain for validation further CHDChain hdChainPrev = hdChainTmp; @@ -409,17 +403,10 @@ void LegacyScriptPubKeyMan::GenerateNewCryptedHDChain(const SecureString& secure res = GetHDChain(hdChainCrypted); assert(res); - DBG( - tfm::format(std::cout, "GenerateNewCryptedHDChain -- current seed: '%s'\n", HexStr(hdChainTmp.GetSeed())); - tfm::format(std::cout, "GenerateNewCryptedHDChain -- crypted seed: '%s'\n", HexStr(hdChainCrypted.GetSeed())); - ); - // ids should match, seed hashes should not assert(hdChainPrev.GetID() == hdChainCrypted.GetID()); assert(hdChainPrev.GetSeedHash() != hdChainCrypted.GetSeedHash()); - hdChainCrypted.Debug(__func__); - if (!SetHDChainSingle(hdChainCrypted, false)) { throw std::runtime_error(std::string(__func__) + ": SetHDChainSingle failed"); } @@ -438,7 +425,6 @@ void LegacyScriptPubKeyMan::GenerateNewHDChain(const SecureString& secureMnemoni // add default account newHdChain.AddAccount(); - newHdChain.Debug(__func__); if (!SetHDChainSingle(newHdChain, false)) { throw std::runtime_error(std::string(__func__) + ": SetHDChainSingle failed"); @@ -518,7 +504,6 @@ bool LegacyScriptPubKeyMan::EncryptHDChain(const CKeyingMaterial& vMasterKeyIn, return false; CHDChain cryptedChain = chain; - cryptedChain.Debug(__func__); cryptedChain.SetCrypted(true); SecureVector vchSecureCryptedSeed(vchCryptedSeed.begin(), vchCryptedSeed.end()); @@ -595,7 +580,6 @@ bool LegacyScriptPubKeyMan::DecryptHDChain(const CKeyingMaterial& vMasterKeyIn, } hdChainRet.SetCrypted(false); - hdChainRet.Debug(__func__); return true; } diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 6be105ba06..757c663df8 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1668,7 +1668,7 @@ void CWallet::NewKeyPoolCallback() { // Note: GetClient(*this) can return nullptr when this wallet is in the middle of its creation. // Skipping stopMixing() is fine in this case. - if (std::unique_ptr coinjoin_client = coinjoin_loader().GetClient(GetName())) { + if (std::unique_ptr coinjoin_client = coinjoin_available() ? coinjoin_loader().GetClient(GetName()) : nullptr) { coinjoin_client->stopMixing(); } nKeysLeftSinceAutoBackup = 0; @@ -4607,7 +4607,6 @@ std::shared_ptr CWallet::Create(interfaces::Chain& chain, interfaces::C } // add default account newHdChain.AddAccount(); - newHdChain.Debug(__func__); } else { if (gArgs.IsArgSet("-hdseed") && !IsHex(strSeed)) { walletInstance->WalletLogPrintf("%s -- Incorrect seed, generating a random mnemonic instead\n", __func__); diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 4e8f82b800..5a6c36f2e1 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -897,6 +897,8 @@ public: /** Interface for accessing CoinJoin state. */ interfaces::CoinJoin::Loader& coinjoin_loader() { assert(m_coinjoin_loader); return *m_coinjoin_loader; } + /** Interface for availability status of CoinJoin. */ + bool coinjoin_available() { return m_coinjoin_loader != nullptr; } const CWalletTx* GetWalletTx(const uint256& hash) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool IsTrusted(const CWalletTx& wtx, std::set& trusted_parents) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); diff --git a/src/wallet/wallettool.cpp b/src/wallet/wallettool.cpp index c7af1d56f3..e3f3fe59b4 100644 --- a/src/wallet/wallettool.cpp +++ b/src/wallet/wallettool.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -21,32 +22,37 @@ static void WalletToolReleaseWallet(CWallet* wallet) delete wallet; } -static void WalletCreate(CWallet* wallet_instance) +static const bool DEFAULT_USE_HD_WALLET{true}; + +static void WalletCreate(CWallet* wallet_instance, uint64_t wallet_creation_flags) { LOCK(wallet_instance->cs_wallet); - wallet_instance->SetMinVersion(FEATURE_COMPRPUBKEY); + if (gArgs.GetBoolArg("-usehd", DEFAULT_USE_HD_WALLET)) { + wallet_instance->SetMinVersion(FEATURE_LATEST); + } else { + wallet_instance->SetMinVersion(FEATURE_COMPRPUBKEY); + } + wallet_instance->SetWalletFlag(wallet_creation_flags); - // generate a new HD seed - wallet_instance->SetupLegacyScriptPubKeyMan(); - auto spk_man = wallet_instance->GetLegacyScriptPubKeyMan(); - // NOTE: drop this condition after removing option to create non-HD wallets - if (spk_man->IsHDEnabled()) { - spk_man->GenerateNewHDChain(/*secureMnemonic=*/"", /*secureMnemonicPassphrase=*/""); + if (!wallet_instance->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) { + // TODO: use here SetupGeneration instead, such as: spk_man->SetupGeneration(false); + // SetupGeneration is not backported yet + wallet_instance->SetupLegacyScriptPubKeyMan(); + auto spk_man = wallet_instance->GetOrCreateLegacyScriptPubKeyMan(); + if (gArgs.GetBoolArg("-usehd", DEFAULT_USE_HD_WALLET)) { + spk_man->GenerateNewHDChain(/*secureMnemonic=*/"", /*secureMnemonicPassphrase=*/""); + } + } else { + wallet_instance->SetupDescriptorScriptPubKeyMans(); } tfm::format(std::cout, "Topping up keypool...\n"); wallet_instance->TopUpKeyPool(); } -static std::shared_ptr MakeWallet(const std::string& name, const fs::path& path, bool create) +static std::shared_ptr MakeWallet(const std::string& name, const fs::path& path, DatabaseOptions options) { - DatabaseOptions options; DatabaseStatus status; - if (create) { - options.require_create = true; - } else { - options.require_existing = true; - } bilingual_str error; std::unique_ptr database = MakeDatabase(path, options, status, error); if (!database) { @@ -87,7 +93,7 @@ static std::shared_ptr MakeWallet(const std::string& name, const fs::pa } } - if (create) WalletCreate(wallet_instance.get()); + if (options.require_create) WalletCreate(wallet_instance.get(), options.create_flags); return wallet_instance; } @@ -109,67 +115,113 @@ static void WalletShowInfo(CWallet* wallet_instance) tfm::format(std::cout, "Address Book: %zu\n", wallet_instance->m_address_book.size()); } -bool ExecuteWalletToolFunc(const std::string& command, const std::string& name) +bool ExecuteWalletToolFunc(const ArgsManager& args, const std::string& command, const std::string& name) { const fs::path path = fsbridge::AbsPathJoin(GetWalletDir(), name); + if (args.IsArgSet("-format") && command != "createfromdump") { + tfm::format(std::cerr, "The -format option can only be used with the \"createfromdump\" command.\n"); + return false; + } + if (args.IsArgSet("-dumpfile") && command != "dump" && command != "createfromdump") { + tfm::format(std::cerr, "The -dumpfile option can only be used with the \"dump\" and \"createfromdump\" commands.\n"); + return false; + } + if (args.IsArgSet("-descriptors") && command != "create") { + tfm::format(std::cerr, "The -descriptors option can only be used with the 'create' command.\n"); + return false; + } + if (command == "create") { - std::shared_ptr wallet_instance = MakeWallet(name, path, /* create= */ true); + DatabaseOptions options; + options.require_create = true; + if (args.GetBoolArg("-descriptors", false)) { + options.create_flags |= WALLET_FLAG_DESCRIPTORS; + options.require_format = DatabaseFormat::SQLITE; + } + + std::shared_ptr wallet_instance = MakeWallet(name, path, options); if (wallet_instance) { WalletShowInfo(wallet_instance.get()); wallet_instance->Close(); } - } else if (command == "info" || command == "salvage" || command == "wipetxes") { - if (command == "info") { - std::shared_ptr wallet_instance = MakeWallet(name, path, /* create= */ false); + } else if (command == "info") { + DatabaseOptions options; + options.require_existing = true; + std::shared_ptr wallet_instance = MakeWallet(name, path, options); if (!wallet_instance) return false; WalletShowInfo(wallet_instance.get()); wallet_instance->Close(); - } else if (command == "salvage") { + } else if (command == "salvage") { #ifdef USE_BDB - bilingual_str error; - std::vector warnings; - bool ret = RecoverDatabaseFile(path, error, warnings); - if (!ret) { - for (const auto& warning : warnings) { - tfm::format(std::cerr, "%s\n", warning.original); - } - if (!error.empty()) { - tfm::format(std::cerr, "%s\n", error.original); - } + bilingual_str error; + std::vector warnings; + bool ret = RecoverDatabaseFile(path, error, warnings); + if (!ret) { + for (const auto& warning : warnings) { + tfm::format(std::cerr, "%s\n", warning.original); } - return ret; -#else - tfm::format(std::cerr, "Salvage command is not available as BDB support is not compiled"); - return false; -#endif - } else if (command == "wipetxes") { -#ifdef USE_BDB - std::shared_ptr wallet_instance = MakeWallet(name, path, /* create= */ false); - if (wallet_instance == nullptr) return false; - - std::vector vHash; - std::vector vHashOut; - - LOCK(wallet_instance->cs_wallet); - - for (auto& [txid, _] : wallet_instance->mapWallet) { - vHash.push_back(txid); + if (!error.empty()) { + tfm::format(std::cerr, "%s\n", error.original); } - - if (wallet_instance->ZapSelectTx(vHash, vHashOut) != DBErrors::LOAD_OK) { - tfm::format(std::cerr, "Could not properly delete transactions"); - wallet_instance->Close(); - return false; - } - - wallet_instance->Close(); - return vHashOut.size() == vHash.size(); -#else - tfm::format(std::cerr, "Wipetxes command is not available as BDB support is not compiled"); - return false; -#endif } + return ret; +#else + tfm::format(std::cerr, "Salvage command is not available as BDB support is not compiled"); + return false; +#endif + } else if (command == "wipetxes") { +#ifdef USE_BDB + DatabaseOptions options; + options.require_existing = true; + std::shared_ptr wallet_instance = MakeWallet(name, path, options); + if (wallet_instance == nullptr) return false; + + std::vector vHash; + std::vector vHashOut; + + LOCK(wallet_instance->cs_wallet); + + for (auto& [txid, _] : wallet_instance->mapWallet) { + vHash.push_back(txid); + } + + if (wallet_instance->ZapSelectTx(vHash, vHashOut) != DBErrors::LOAD_OK) { + tfm::format(std::cerr, "Could not properly delete transactions"); + wallet_instance->Close(); + return false; + } + + wallet_instance->Close(); + return vHashOut.size() == vHash.size(); +#else + tfm::format(std::cerr, "Wipetxes command is not available as BDB support is not compiled"); + return false; +#endif + } else if (command == "dump") { + DatabaseOptions options; + options.require_existing = true; + std::shared_ptr wallet_instance = MakeWallet(name, path, options); + if (!wallet_instance) return false; + bilingual_str error; + bool ret = DumpWallet(*wallet_instance, error); + if (!ret && !error.empty()) { + tfm::format(std::cerr, "%s\n", error.original); + return ret; + } + 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; diff --git a/src/wallet/wallettool.h b/src/wallet/wallettool.h index d0b8d6812a..6a9a810231 100644 --- a/src/wallet/wallettool.h +++ b/src/wallet/wallettool.h @@ -10,7 +10,7 @@ namespace WalletTool { void WalletShowInfo(CWallet* wallet_instance); -bool ExecuteWalletToolFunc(const std::string& command, const std::string& file); +bool ExecuteWalletToolFunc(const ArgsManager& args, const std::string& command, const std::string& file); } // namespace WalletTool diff --git a/test/functional/test_framework/address.py b/test/functional/test_framework/address.py index 3622017288..1312cff489 100644 --- a/test/functional/test_framework/address.py +++ b/test/functional/test_framework/address.py @@ -12,9 +12,7 @@ import unittest from .script import hash160, hash256, CScript -from .util import hex_str_to_bytes - -from test_framework.util import assert_equal +from .util import assert_equal, hex_str_to_bytes # Note unlike in bitcoin, this address isn't bech32 since we don't (at this time) support bech32. ADDRESS_BCRT1_UNSPENDABLE = 'yVg3NBUHNEhgDceqwVUjsZHreC5PBHnUo9' @@ -31,7 +29,7 @@ def byte_to_base58(b, version): str = chr(version).encode('latin-1').hex() + str checksum = hash256(hex_str_to_bytes(str)).hex() str += checksum[:8] - value = int('0x'+str,0) + value = int('0x' + str, 0) while value > 0: result = chars[value % 58] + result value //= 58 @@ -41,7 +39,10 @@ def byte_to_base58(b, version): return result -def base58_to_byte(s, verify_checksum=True): +def base58_to_byte(s): + """Converts a base58-encoded string to its data and version. + + Throws if the base58 checksum is invalid.""" if not s: return b'' n = 0 @@ -61,40 +62,41 @@ def base58_to_byte(s, verify_checksum=True): else: break res = b'\x00' * pad + res - if verify_checksum: - assert_equal(hash256(res[:-4])[:4], res[-4:]) + + # Assert if the checksum is invalid + assert_equal(hash256(res[:-4])[:4], res[-4:]) return res[1:-4], int(res[0]) -def keyhash_to_p2pkh(hash, main = False): +def keyhash_to_p2pkh(hash, main=False): assert len(hash) == 20 version = 76 if main else 140 return byte_to_base58(hash, version) -def scripthash_to_p2sh(hash, main = False): +def scripthash_to_p2sh(hash, main=False): assert len(hash) == 20 version = 16 if main else 19 return byte_to_base58(hash, version) -def key_to_p2pkh(key, main = False): +def key_to_p2pkh(key, main=False): key = check_key(key) return keyhash_to_p2pkh(hash160(key), main) -def script_to_p2sh(script, main = False): +def script_to_p2sh(script, main=False): script = check_script(script) return scripthash_to_p2sh(hash160(script), main) def check_key(key): if (type(key) is str): - key = hex_str_to_bytes(key) # Assuming this is hex string + key = hex_str_to_bytes(key) # Assuming this is hex string if (type(key) is bytes and (len(key) == 33 or len(key) == 65)): return key assert False def check_script(script): if (type(script) is str): - script = hex_str_to_bytes(script) # Assuming this is hex string + script = hex_str_to_bytes(script) # Assuming this is hex string if (type(script) is bytes or type(script) is CScript): return script assert False @@ -105,15 +107,15 @@ class TestFrameworkScript(unittest.TestCase): def check_base58(data, version): self.assertEqual(base58_to_byte(byte_to_base58(data, version)), (data, version)) - check_base58(b'\x1f\x8e\xa1p*{\xd4\x94\x1b\xca\tA\xb8R\xc4\xbb\xfe\xdb.\x05', 111) - check_base58(b':\x0b\x05\xf4\xd7\xf6l;\xa7\x00\x9fE50)l\x84\\\xc9\xcf', 111) - check_base58(b'A\xc1\xea\xf1\x11\x80%Y\xba\xd6\x1b`\xd6+\x1f\x89|c\x92\x8a', 111) - check_base58(b'\0A\xc1\xea\xf1\x11\x80%Y\xba\xd6\x1b`\xd6+\x1f\x89|c\x92\x8a', 111) - check_base58(b'\0\0A\xc1\xea\xf1\x11\x80%Y\xba\xd6\x1b`\xd6+\x1f\x89|c\x92\x8a', 111) - check_base58(b'\0\0\0A\xc1\xea\xf1\x11\x80%Y\xba\xd6\x1b`\xd6+\x1f\x89|c\x92\x8a', 111) - check_base58(b'\x1f\x8e\xa1p*{\xd4\x94\x1b\xca\tA\xb8R\xc4\xbb\xfe\xdb.\x05', 0) - check_base58(b':\x0b\x05\xf4\xd7\xf6l;\xa7\x00\x9fE50)l\x84\\\xc9\xcf', 0) - check_base58(b'A\xc1\xea\xf1\x11\x80%Y\xba\xd6\x1b`\xd6+\x1f\x89|c\x92\x8a', 0) - check_base58(b'\0A\xc1\xea\xf1\x11\x80%Y\xba\xd6\x1b`\xd6+\x1f\x89|c\x92\x8a', 0) - check_base58(b'\0\0A\xc1\xea\xf1\x11\x80%Y\xba\xd6\x1b`\xd6+\x1f\x89|c\x92\x8a', 0) - check_base58(b'\0\0\0A\xc1\xea\xf1\x11\x80%Y\xba\xd6\x1b`\xd6+\x1f\x89|c\x92\x8a', 0) + check_base58(bytes.fromhex('1f8ea1702a7bd4941bca0941b852c4bbfedb2e05'), 111) + check_base58(bytes.fromhex('3a0b05f4d7f66c3ba7009f453530296c845cc9cf'), 111) + check_base58(bytes.fromhex('41c1eaf111802559bad61b60d62b1f897c63928a'), 111) + check_base58(bytes.fromhex('0041c1eaf111802559bad61b60d62b1f897c63928a'), 111) + check_base58(bytes.fromhex('000041c1eaf111802559bad61b60d62b1f897c63928a'), 111) + check_base58(bytes.fromhex('00000041c1eaf111802559bad61b60d62b1f897c63928a'), 111) + check_base58(bytes.fromhex('1f8ea1702a7bd4941bca0941b852c4bbfedb2e05'), 0) + check_base58(bytes.fromhex('3a0b05f4d7f66c3ba7009f453530296c845cc9cf'), 0) + check_base58(bytes.fromhex('41c1eaf111802559bad61b60d62b1f897c63928a'), 0) + check_base58(bytes.fromhex('0041c1eaf111802559bad61b60d62b1f897c63928a'), 0) + check_base58(bytes.fromhex('000041c1eaf111802559bad61b60d62b1f897c63928a'), 0) + check_base58(bytes.fromhex('00000041c1eaf111802559bad61b60d62b1f897c63928a'), 0) diff --git a/test/functional/tool_wallet.py b/test/functional/tool_wallet.py index 6fee23ea4d..6af413fefb 100755 --- a/test/functional/tool_wallet.py +++ b/test/functional/tool_wallet.py @@ -10,6 +10,8 @@ import stat import subprocess import textwrap +from collections import OrderedDict + from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal @@ -27,8 +29,11 @@ class ToolWalletTest(BitcoinTestFramework): def dash_wallet_process(self, *args): binary = self.config["environment"]["BUILDDIR"] + '/src/dash-wallet' + self.config["environment"]["EXEEXT"] - args = ['-datadir={}'.format(self.nodes[0].datadir), '-regtest'] + list(args) - return subprocess.Popen([binary] + args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + default_args = ['-datadir={}'.format(self.nodes[0].datadir), '-chain=%s' % self.chain] + if self.options.descriptors and 'create' in args: + default_args.append('-descriptors') + + return subprocess.Popen([binary] + default_args + list(args), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) def assert_raises_tool_error(self, error, *args): p = self.dash_wallet_process(*args) @@ -62,6 +67,119 @@ class ToolWalletTest(BitcoinTestFramework): result = 'unchanged' if new == old else 'increased!' self.log.debug('Wallet file timestamp {}'.format(result)) + def get_expected_info_output(self, name="", transactions=0, keypool=2, address=0): + wallet_name = self.default_wallet_name if name == "" else name + output_types = 1 # p2pkh + if self.options.descriptors: + return textwrap.dedent('''\ + Wallet info + =========== + Name: %s + Format: sqlite + Descriptors: yes + Encrypted: no + HD (hd seed available): yes + Keypool Size: %d + Transactions: %d + Address Book: %d + ''' % (wallet_name, keypool * output_types, transactions, address)) + else: + return textwrap.dedent('''\ + Wallet info + =========== + Name: %s + Format: bdb + Descriptors: no + Encrypted: no + HD (hd seed available): yes + Keypool Size: %d + Transactions: %d + Address Book: %d + ''' % (wallet_name, keypool, transactions, address * output_types)) + + def read_dump(self, filename): + dump = OrderedDict() + with open(filename, "r", encoding="utf8") as f: + for row in f: + row = row.strip() + key, value = row.split(',') + dump[key] = value + return dump + + def assert_is_sqlite(self, filename): + with open(filename, 'rb') as f: + file_magic = f.read(16) + assert file_magic == b'SQLite format 3\x00' + + def assert_is_bdb(self, filename): + with open(filename, 'rb') as f: + f.seek(12, 0) + file_magic = f.read(4) + assert file_magic == b'\x00\x05\x31\x62' or file_magic == b'\x62\x31\x05\x00' + + def write_dump(self, dump, filename, magic=None, skip_checksum=False): + if magic is None: + magic = "BITCOIN_CORE_WALLET_DUMP" + with open(filename, "w", encoding="utf8") as f: + row = ",".join([magic, dump[magic]]) + "\n" + f.write(row) + for k, v in dump.items(): + if k == magic or k == "checksum": + continue + row = ",".join([k, v]) + "\n" + f.write(row) + if not skip_checksum: + row = ",".join(["checksum", dump["checksum"]]) + "\n" + f.write(row) + + def assert_dump(self, expected, received): + e = expected.copy() + r = received.copy() + + # BDB will add a "version" record that is not present in sqlite + # In that case, we should ignore this record in both + # But because this also effects the checksum, we also need to drop that. + v_key = "0776657273696f6e" # Version key + if v_key in e and v_key not in r: + del e[v_key] + del e["checksum"] + del r["checksum"] + if v_key not in e and v_key in r: + del r[v_key] + del e["checksum"] + del r["checksum"] + + assert_equal(len(e), len(r)) + for k, v in e.items(): + assert_equal(v, r[k]) + + def do_tool_createfromdump(self, wallet_name, dumpfile, file_format=None): + dumppath = os.path.join(self.nodes[0].datadir, dumpfile) + rt_dumppath = os.path.join(self.nodes[0].datadir, "rt-{}.dump".format(wallet_name)) + + dump_data = self.read_dump(dumppath) + + args = ["-wallet={}".format(wallet_name), + "-dumpfile={}".format(dumppath)] + if file_format is not None: + args.append("-format={}".format(file_format)) + args.append("createfromdump") + + load_output = "" + if file_format is not None and file_format != dump_data["format"]: + load_output += "Warning: Dumpfile wallet format \"{}\" does not match command line specified format \"{}\".\n".format(dump_data["format"], file_format) + self.assert_tool_output(load_output, *args) + assert os.path.isdir(os.path.join(self.nodes[0].datadir, "regtest/wallets", wallet_name)) + + self.assert_tool_output("The dumpfile may contain private keys. To ensure the safety of your Bitcoin, do not share the dumpfile.\n", '-wallet={}'.format(wallet_name), '-dumpfile={}'.format(rt_dumppath), 'dump') + + rt_dump_data = self.read_dump(rt_dumppath) + wallet_dat = os.path.join(self.nodes[0].datadir, "regtest/wallets/", wallet_name, "wallet.dat") + if rt_dump_data["format"] == "bdb": + self.assert_is_bdb(wallet_dat) + else: + self.assert_is_sqlite(wallet_dat) + def test_invalid_tool_commands_and_args(self): self.log.info('Testing that various invalid commands raise with specific error messages') self.assert_raises_tool_error('Invalid command: foo', 'foo') @@ -97,33 +215,7 @@ class ToolWalletTest(BitcoinTestFramework): # shasum_before = self.wallet_shasum() timestamp_before = self.wallet_timestamp() self.log.debug('Wallet file timestamp before calling info: {}'.format(timestamp_before)) - if self.options.descriptors: - out = textwrap.dedent('''\ - Wallet info - =========== - Name: default_wallet - Format: sqlite - Descriptors: yes - Encrypted: no - HD (hd seed available): yes - Keypool Size: 2 - Transactions: 0 - Address Book: 1 - ''') - else: - out = textwrap.dedent('''\ - Wallet info - =========== - Name: \ - - Format: bdb - Descriptors: no - Encrypted: no - HD (hd seed available): yes - Keypool Size: 2 - Transactions: 0 - Address Book: 1 - ''') + out = self.get_expected_info_output(address=1) self.assert_tool_output(out, '-wallet=' + self.default_wallet_name, 'info') timestamp_after = self.wallet_timestamp() @@ -155,33 +247,7 @@ class ToolWalletTest(BitcoinTestFramework): shasum_before = self.wallet_shasum() timestamp_before = self.wallet_timestamp() self.log.debug('Wallet file timestamp before calling info: {}'.format(timestamp_before)) - if self.options.descriptors: - out = textwrap.dedent('''\ - Wallet info - =========== - Name: default_wallet - Format: sqlite - Descriptors: yes - Encrypted: no - HD (hd seed available): yes - Keypool Size: 2 - Transactions: 1 - Address Book: 1 - ''') - else: - out = textwrap.dedent('''\ - Wallet info - =========== - Name: \ - - Format: bdb - Descriptors: no - Encrypted: no - HD (hd seed available): yes - Keypool Size: 2 - Transactions: 1 - Address Book: 1 - ''') + out = self.get_expected_info_output(transactions=1, address=1) self.assert_tool_output(out, '-wallet=' + self.default_wallet_name, 'info') shasum_after = self.wallet_shasum() timestamp_after = self.wallet_timestamp() @@ -199,19 +265,7 @@ class ToolWalletTest(BitcoinTestFramework): shasum_before = self.wallet_shasum() timestamp_before = self.wallet_timestamp() self.log.debug('Wallet file timestamp before calling create: {}'.format(timestamp_before)) - out = textwrap.dedent('''\ - Topping up keypool... - Wallet info - =========== - Name: foo - Format: bdb - Descriptors: no - Encrypted: no - HD (hd seed available): no - Keypool Size: 1000 - Transactions: 0 - Address Book: 0 - ''') + out = "Topping up keypool...\n" + self.get_expected_info_output(name="foo", keypool=2000) self.assert_tool_output(out, '-wallet=foo', 'create') shasum_after = self.wallet_shasum() timestamp_after = self.wallet_timestamp() @@ -237,7 +291,13 @@ class ToolWalletTest(BitcoinTestFramework): self.log.debug('Wallet file timestamp after calling getwalletinfo: {}'.format(timestamp_after)) assert_equal(0, out['txcount']) - assert_equal(1000, out['keypoolsize']) + if not self.options.descriptors: + assert_equal(1000, out['keypoolsize']) + assert_equal(1000, out['keypoolsize_hd_internal']) + assert_equal(True, 'hdchainid' in out) + else: + assert_equal(1000, out['keypoolsize']) + assert_equal(1000, out['keypoolsize_hd_internal']) self.log_wallet_timestamp_comparison(timestamp_before, timestamp_after) assert_equal(timestamp_before, timestamp_after) @@ -254,51 +314,116 @@ class ToolWalletTest(BitcoinTestFramework): self.assert_tool_output('', '-wallet=salvage', 'salvage') def test_wipe(self): - out = textwrap.dedent('''\ - Wallet info - =========== - Name: \ - - Format: bdb - Descriptors: no - Encrypted: no - HD (hd seed available): yes - Keypool Size: 2 - Transactions: 1 - Address Book: 1 - ''') + out = self.get_expected_info_output(transactions=1, address=1) self.assert_tool_output(out, '-wallet=' + self.default_wallet_name, 'info') self.assert_tool_output('', '-wallet=' + self.default_wallet_name, 'wipetxes') - out = textwrap.dedent('''\ - Wallet info - =========== - Name: \ - - Format: bdb - Descriptors: no - Encrypted: no - HD (hd seed available): yes - Keypool Size: 2 - Transactions: 0 - Address Book: 1 - ''') + out = self.get_expected_info_output(transactions=0, address=1) self.assert_tool_output(out, '-wallet=' + self.default_wallet_name, 'info') + def test_dump_createfromdump(self): + self.start_node(0) + self.nodes[0].createwallet("todump") + file_format = self.nodes[0].get_wallet_rpc("todump").getwalletinfo()["format"] + self.nodes[0].createwallet("todump2") + self.stop_node(0) + + self.log.info('Checking dump arguments') + self.assert_raises_tool_error('No dump file provided. To use dump, -dumpfile= must be provided.', '-wallet=todump', 'dump') + + self.log.info('Checking basic dump') + wallet_dump = os.path.join(self.nodes[0].datadir, "wallet.dump") + self.assert_tool_output('The dumpfile may contain private keys. To ensure the safety of your Bitcoin, do not share the dumpfile.\n', '-wallet=todump', '-dumpfile={}'.format(wallet_dump), 'dump') + + dump_data = self.read_dump(wallet_dump) + orig_dump = dump_data.copy() + # Check the dump magic + assert_equal(dump_data['BITCOIN_CORE_WALLET_DUMP'], '1') + # Check the file format + assert_equal(dump_data["format"], file_format) + + self.log.info('Checking that a dumpfile cannot be overwritten') + self.assert_raises_tool_error('File {} already exists. If you are sure this is what you want, move it out of the way first.'.format(wallet_dump), '-wallet=todump2', '-dumpfile={}'.format(wallet_dump), 'dump') + + self.log.info('Checking createfromdump arguments') + self.assert_raises_tool_error('No dump file provided. To use createfromdump, -dumpfile= must be provided.', '-wallet=todump', 'createfromdump') + non_exist_dump = os.path.join(self.nodes[0].datadir, "wallet.nodump") + self.assert_raises_tool_error('Unknown wallet file format "notaformat" provided. Please provide one of "bdb" or "sqlite".', '-wallet=todump', '-format=notaformat', '-dumpfile={}'.format(wallet_dump), 'createfromdump') + self.assert_raises_tool_error('Dump file {} does not exist.'.format(non_exist_dump), '-wallet=todump', '-dumpfile={}'.format(non_exist_dump), 'createfromdump') + wallet_path = os.path.join(self.nodes[0].datadir, 'regtest/wallets/todump2') + self.assert_raises_tool_error('Failed to create database path \'{}\'. Database already exists.'.format(wallet_path), '-wallet=todump2', '-dumpfile={}'.format(wallet_dump), 'createfromdump') + self.assert_raises_tool_error("The -descriptors option can only be used with the 'create' command.", '-descriptors', '-wallet=todump2', '-dumpfile={}'.format(wallet_dump), 'createfromdump') + + self.log.info('Checking createfromdump') + self.do_tool_createfromdump("load", "wallet.dump") + if self.is_bdb_compiled(): + self.do_tool_createfromdump("load-bdb", "wallet.dump", "bdb") + if self.is_sqlite_compiled(): + self.do_tool_createfromdump("load-sqlite", "wallet.dump", "sqlite") + + self.log.info('Checking createfromdump handling of magic and versions') + bad_ver_wallet_dump = os.path.join(self.nodes[0].datadir, "wallet-bad_ver1.dump") + dump_data["BITCOIN_CORE_WALLET_DUMP"] = "0" + self.write_dump(dump_data, bad_ver_wallet_dump) + self.assert_raises_tool_error('Error: Dumpfile version is not supported. This version of bitcoin-wallet only supports version 1 dumpfiles. Got dumpfile with version 0', '-wallet=badload', '-dumpfile={}'.format(bad_ver_wallet_dump), 'createfromdump') + assert not os.path.isdir(os.path.join(self.nodes[0].datadir, "regtest/wallets", "badload")) + bad_ver_wallet_dump = os.path.join(self.nodes[0].datadir, "wallet-bad_ver2.dump") + dump_data["BITCOIN_CORE_WALLET_DUMP"] = "2" + self.write_dump(dump_data, bad_ver_wallet_dump) + self.assert_raises_tool_error('Error: Dumpfile version is not supported. This version of bitcoin-wallet only supports version 1 dumpfiles. Got dumpfile with version 2', '-wallet=badload', '-dumpfile={}'.format(bad_ver_wallet_dump), 'createfromdump') + assert not os.path.isdir(os.path.join(self.nodes[0].datadir, "regtest/wallets", "badload")) + bad_magic_wallet_dump = os.path.join(self.nodes[0].datadir, "wallet-bad_magic.dump") + del dump_data["BITCOIN_CORE_WALLET_DUMP"] + dump_data["not_the_right_magic"] = "1" + self.write_dump(dump_data, bad_magic_wallet_dump, "not_the_right_magic") + self.assert_raises_tool_error('Error: Dumpfile identifier record is incorrect. Got "not_the_right_magic", expected "BITCOIN_CORE_WALLET_DUMP".', '-wallet=badload', '-dumpfile={}'.format(bad_magic_wallet_dump), 'createfromdump') + assert not os.path.isdir(os.path.join(self.nodes[0].datadir, "regtest/wallets", "badload")) + + self.log.info('Checking createfromdump handling of checksums') + bad_sum_wallet_dump = os.path.join(self.nodes[0].datadir, "wallet-bad_sum1.dump") + dump_data = orig_dump.copy() + checksum = dump_data["checksum"] + dump_data["checksum"] = "1" * 64 + self.write_dump(dump_data, bad_sum_wallet_dump) + self.assert_raises_tool_error('Error: Dumpfile checksum does not match. Computed {}, expected {}'.format(checksum, "1" * 64), '-wallet=bad', '-dumpfile={}'.format(bad_sum_wallet_dump), 'createfromdump') + assert not os.path.isdir(os.path.join(self.nodes[0].datadir, "regtest/wallets", "badload")) + bad_sum_wallet_dump = os.path.join(self.nodes[0].datadir, "wallet-bad_sum2.dump") + del dump_data["checksum"] + self.write_dump(dump_data, bad_sum_wallet_dump, skip_checksum=True) + self.assert_raises_tool_error('Error: Missing checksum', '-wallet=badload', '-dumpfile={}'.format(bad_sum_wallet_dump), 'createfromdump') + assert not os.path.isdir(os.path.join(self.nodes[0].datadir, "regtest/wallets", "badload")) + bad_sum_wallet_dump = os.path.join(self.nodes[0].datadir, "wallet-bad_sum3.dump") + dump_data["checksum"] = "2" * 10 + self.write_dump(dump_data, bad_sum_wallet_dump) + self.assert_raises_tool_error('Error: Dumpfile checksum does not match. Computed {}, expected {}{}'.format(checksum, "2" * 10, "0" * 54), '-wallet=badload', '-dumpfile={}'.format(bad_sum_wallet_dump), 'createfromdump') + assert not os.path.isdir(os.path.join(self.nodes[0].datadir, "regtest/wallets", "badload")) + + + def test_nonhd(self): + self.log.info('Check non-hd wallet') + self.start_node(0, ['-usehd=0', '-nowallet']) + self.nodes[0].createwallet("nohd") + assert_equal(False, 'hdchainid' in self.nodes[0].get_wallet_rpc('nohd').getwalletinfo()) + self.restart_node(0, ['-usehd=1', '-nowallet']) + self.nodes[0].createwallet("hd") + assert_equal(True, 'hdchainid' in self.nodes[0].get_wallet_rpc('hd').getwalletinfo()) + self.stop_node(0) + def run_test(self): self.wallet_path = os.path.join(self.nodes[0].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename) self.test_invalid_tool_commands_and_args() # Warning: The following tests are order-dependent. self.test_tool_wallet_info() self.test_tool_wallet_info_after_transaction() + self.test_tool_wallet_create_on_existing_wallet() + self.test_getwalletinfo_on_different_wallet() if not self.options.descriptors: - # TODO: Wallet tool needs more create options at which point these can be enabled. - self.test_tool_wallet_create_on_existing_wallet() - self.test_getwalletinfo_on_different_wallet() # Salvage is a legacy wallet only thing self.test_salvage() self.test_wipe() + self.test_nonhd() + self.test_dump_createfromdump() if __name__ == '__main__': ToolWalletTest().main()