bitcoin/src/wallet/test/db_tests.cpp

390 lines
15 KiB
C++
Raw Normal View History

// Copyright (c) 2018-2022 The Bitcoin Core developers
2018-09-14 21:10:20 -04:00
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <bitcoin-build-config.h> // IWYU pragma: keep
scripted-diff: Fix bitcoin_config_h includes -BEGIN VERIFY SCRIPT- regex_string='^(?!//).*(AC_APPLE_UNIVERSAL_BUILD|BOOST_PROCESS_USE_STD_FS|CHAR_EQUALS_INT8|CLIENT_VERSION_BUILD|CLIENT_VERSION_IS_RELEASE|CLIENT_VERSION_MAJOR|CLIENT_VERSION_MINOR|COPYRIGHT_HOLDERS|COPYRIGHT_HOLDERS_FINAL|COPYRIGHT_HOLDERS_SUBSTITUTION|COPYRIGHT_YEAR|ENABLE_ARM_SHANI|ENABLE_AVX2|ENABLE_EXTERNAL_SIGNER|ENABLE_SSE41|ENABLE_TRACING|ENABLE_WALLET|ENABLE_X86_SHANI|ENABLE_ZMQ|HAVE_BOOST|HAVE_BUILTIN_CLZL|HAVE_BUILTIN_CLZLL|HAVE_BYTESWAP_H|HAVE_CLMUL|HAVE_CONSENSUS_LIB|HAVE_CXX20|HAVE_DECL_BE16TOH|HAVE_DECL_BE32TOH|HAVE_DECL_BE64TOH|HAVE_DECL_BSWAP_16|HAVE_DECL_BSWAP_32|HAVE_DECL_BSWAP_64|HAVE_DECL_FORK|HAVE_DECL_FREEIFADDRS|HAVE_DECL_GETIFADDRS|HAVE_DECL_HTOBE16|HAVE_DECL_HTOBE32|HAVE_DECL_HTOBE64|HAVE_DECL_HTOLE16|HAVE_DECL_HTOLE32|HAVE_DECL_HTOLE64|HAVE_DECL_LE16TOH|HAVE_DECL_LE32TOH|HAVE_DECL_LE64TOH|HAVE_DECL_PIPE2|HAVE_DECL_SETSID|HAVE_DECL_STRERROR_R|HAVE_DEFAULT_VISIBILITY_ATTRIBUTE|HAVE_DLFCN_H|HAVE_DLLEXPORT_ATTRIBUTE|HAVE_ENDIAN_H|HAVE_EVHTTP_CONNECTION_GET_PEER_CONST_CHAR|HAVE_FDATASYNC|HAVE_GETENTROPY_RAND|HAVE_GETRANDOM|HAVE_GMTIME_R|HAVE_INTTYPES_H|HAVE_LIBADVAPI32|HAVE_LIBCOMCTL32|HAVE_LIBCOMDLG32|HAVE_LIBGDI32|HAVE_LIBIPHLPAPI|HAVE_LIBKERNEL32|HAVE_LIBOLE32|HAVE_LIBOLEAUT32|HAVE_LIBSHELL32|HAVE_LIBSHLWAPI|HAVE_LIBUSER32|HAVE_LIBUUID|HAVE_LIBWINMM|HAVE_LIBWS2_32|HAVE_MALLOC_INFO|HAVE_MALLOPT_ARENA_MAX|HAVE_MINIUPNPC_MINIUPNPC_H|HAVE_MINIUPNPC_UPNPCOMMANDS_H|HAVE_MINIUPNPC_UPNPERRORS_H|HAVE_NATPMP_H|HAVE_O_CLOEXEC|HAVE_POSIX_FALLOCATE|HAVE_PTHREAD|HAVE_PTHREAD_PRIO_INHERIT|HAVE_STDINT_H|HAVE_STDIO_H|HAVE_STDLIB_H|HAVE_STRERROR_R|HAVE_STRINGS_H|HAVE_STRING_H|HAVE_STRONG_GETAUXVAL|HAVE_SYSCTL|HAVE_SYSCTL_ARND|HAVE_SYSTEM|HAVE_SYS_ENDIAN_H|HAVE_SYS_PRCTL_H|HAVE_SYS_RESOURCES_H|HAVE_SYS_SELECT_H|HAVE_SYS_STAT_H|HAVE_SYS_SYSCTL_H|HAVE_SYS_TYPES_H|HAVE_SYS_VMMETER_H|HAVE_THREAD_LOCAL|HAVE_TIMINGSAFE_BCMP|HAVE_UNISTD_H|HAVE_VM_VM_PARAM_H|LT_OBJDIR|PACKAGE_BUGREPORT|PACKAGE_NAME|PACKAGE_STRING|PACKAGE_TARNAME|PACKAGE_URL|PACKAGE_VERSION|PTHREAD_CREATE_JOINABLE|QT_QPA_PLATFORM_ANDROID|QT_QPA_PLATFORM_COCOA|QT_QPA_PLATFORM_MINIMAL|QT_QPA_PLATFORM_WINDOWS|QT_QPA_PLATFORM_XCB|QT_STATICPLUGIN|STDC_HEADERS|STRERROR_R_CHAR_P|USE_ASM|USE_BDB|USE_DBUS|USE_NATPMP|USE_QRCODE|USE_SQLITE|USE_UPNP|_FILE_OFFSET_BITS|_LARGE_FILES)' exclusion_files=":(exclude)src/minisketch :(exclude)src/crc32c :(exclude)src/secp256k1 :(exclude)src/crypto/sha256_arm_shani.cpp :(exclude)src/crypto/sha256_avx2.cpp :(exclude)src/crypto/sha256_sse41.cpp :(exclude)src/crypto/sha256_x86_shani.cpp" git grep --perl-regexp --files-with-matches "$regex_string" -- '*.cpp' $exclusion_files | xargs git grep -L "bitcoin-config.h" | while read -r file; do line_number=$(awk -v my_file="$file" '/\/\/ file COPYING or https?:\/\/www.opensource.org\/licenses\/mit-license.php\./ {line = NR} /^\/\// && NR == line + 1 {while(getline && /^\/\//) line = NR} END {print line+1}' "$file"); sed -i "${line_number}i\\\\n\#if defined(HAVE_CONFIG_H)\\n#include <config/bitcoin-config.h>\\n\#endif" "$file"; done; git grep --perl-regexp --files-with-matches "$regex_string" -- '*.h' $exclusion_files | xargs git grep -L "bitcoin-config.h" | while read -r file; do sed -i "/#define.*_H/a \\\\n\#if defined(HAVE_CONFIG_H)\\n#include <config/bitcoin-config.h>\\n\#endif" "$file"; done; for file in $(git grep --files-with-matches 'bitcoin-config.h' -- '*.cpp' '*.h' $exclusion_files); do if ! grep -q --perl-regexp "$regex_string" $file; then sed -i '/HAVE_CONFIG_H/{N;N;N;d;}' $file; fi; done; -END VERIFY SCRIPT- The first command creates a regular expression for matching all bitcoin-config.h symbols in the following form: ^(?!//).*(AC_APPLE_UNIVERSAL_BUILD|BOOST_PROCESS_USE_STD_FS|...|_LARGE_FILES). It was generated with: ./autogen.sh && printf '^(?!//).*(%s)' $(awk '/^#undef/ {print $2}' src/config/bitcoin-config.h.in | paste -sd "|" -) The second command holds a list of files and directories that should not be processed. These include subtree directories as well as some crypto files that already get their symbols through the makefile. The third command checks for missing bitcoin-config headers in .cpp files and adds the header if it is missing. The fourth command checks for missing bitcoin-config headers in .h files and adds the header if it is missing. The fifth command checks for unneeded bitcoin-config headers in sources files and removes the header if it is unneeded.
2024-02-13 08:03:02 +01:00
2018-09-14 21:10:20 -04:00
#include <boost/test/unit_test.hpp>
#include <test/util/setup_common.h>
2023-06-14 15:07:36 +01:00
#include <util/check.h>
#include <util/fs.h>
#include <util/translation.h>
#ifdef USE_BDB
#include <wallet/bdb.h>
#endif
#ifdef USE_SQLITE
#include <wallet/sqlite.h>
#endif
#include <wallet/migrate.h>
#include <wallet/test/util.h>
#include <wallet/walletutil.h> // for WALLET_FLAG_DESCRIPTORS
2018-09-14 21:10:20 -04:00
#include <fstream>
#include <memory>
#include <string>
2018-09-14 21:10:20 -04:00
inline std::ostream& operator<<(std::ostream& os, const std::pair<const SerializeData, SerializeData>& kv)
{
Span key{kv.first}, value{kv.second};
os << "(\"" << std::string_view{reinterpret_cast<const char*>(key.data()), key.size()} << "\", \""
<< std::string_view{reinterpret_cast<const char*>(value.data()), value.size()} << "\")";
return os;
}
namespace wallet {
inline std::span<const std::byte> StringBytes(std::string_view str)
{
return std::as_bytes(std::span{str});
}
static SerializeData StringData(std::string_view str)
{
auto bytes = StringBytes(str);
return SerializeData{bytes.begin(), bytes.end()};
}
static void CheckPrefix(DatabaseBatch& batch, Span<const std::byte> prefix, MockableData expected)
{
std::unique_ptr<DatabaseCursor> cursor = batch.GetNewPrefixCursor(prefix);
MockableData actual;
while (true) {
DataStream key, value;
DatabaseCursor::Status status = cursor->Next(key, value);
if (status == DatabaseCursor::Status::DONE) break;
BOOST_CHECK(status == DatabaseCursor::Status::MORE);
BOOST_CHECK(
actual.emplace(SerializeData(key.begin(), key.end()), SerializeData(value.begin(), value.end())).second);
}
BOOST_CHECK_EQUAL_COLLECTIONS(actual.begin(), actual.end(), expected.begin(), expected.end());
}
2018-09-14 21:10:20 -04:00
BOOST_FIXTURE_TEST_SUITE(db_tests, BasicTestingSetup)
static std::shared_ptr<BerkeleyEnvironment> GetWalletEnv(const fs::path& path, fs::path& database_filename)
{
fs::path data_file = BDBDataFile(path);
database_filename = data_file.filename();
return GetBerkeleyEnv(data_file.parent_path(), false);
}
2018-09-14 21:10:20 -04:00
BOOST_AUTO_TEST_CASE(getwalletenv_file)
{
fs::path test_name = "test_name.dat";
const fs::path datadir = m_args.GetDataDirNet();
2018-09-14 21:10:20 -04:00
fs::path file_path = datadir / test_name;
std::ofstream f{file_path};
2018-09-14 21:10:20 -04:00
f.close();
fs::path filename;
2018-09-14 21:10:20 -04:00
std::shared_ptr<BerkeleyEnvironment> env = GetWalletEnv(file_path, filename);
BOOST_CHECK_EQUAL(filename, test_name);
BOOST_CHECK_EQUAL(env->Directory(), datadir);
2018-09-14 21:10:20 -04:00
}
BOOST_AUTO_TEST_CASE(getwalletenv_directory)
{
fs::path expected_name = "wallet.dat";
const fs::path datadir = m_args.GetDataDirNet();
2018-09-14 21:10:20 -04:00
fs::path filename;
2018-09-14 21:10:20 -04:00
std::shared_ptr<BerkeleyEnvironment> env = GetWalletEnv(datadir, filename);
BOOST_CHECK_EQUAL(filename, expected_name);
BOOST_CHECK_EQUAL(env->Directory(), datadir);
2018-09-14 21:10:20 -04:00
}
BOOST_AUTO_TEST_CASE(getwalletenv_g_dbenvs_multiple)
{
fs::path datadir = m_args.GetDataDirNet() / "1";
fs::path datadir_2 = m_args.GetDataDirNet() / "2";
fs::path filename;
2018-09-14 21:10:20 -04:00
std::shared_ptr<BerkeleyEnvironment> env_1 = GetWalletEnv(datadir, filename);
std::shared_ptr<BerkeleyEnvironment> env_2 = GetWalletEnv(datadir, filename);
std::shared_ptr<BerkeleyEnvironment> env_3 = GetWalletEnv(datadir_2, filename);
BOOST_CHECK(env_1 == env_2);
BOOST_CHECK(env_2 != env_3);
}
BOOST_AUTO_TEST_CASE(getwalletenv_g_dbenvs_free_instance)
{
fs::path datadir = gArgs.GetDataDirNet() / "1";
fs::path datadir_2 = gArgs.GetDataDirNet() / "2";
fs::path filename;
2018-09-14 21:10:20 -04:00
std::shared_ptr <BerkeleyEnvironment> env_1_a = GetWalletEnv(datadir, filename);
std::shared_ptr <BerkeleyEnvironment> env_2_a = GetWalletEnv(datadir_2, filename);
env_1_a.reset();
std::shared_ptr<BerkeleyEnvironment> env_1_b = GetWalletEnv(datadir, filename);
std::shared_ptr<BerkeleyEnvironment> env_2_b = GetWalletEnv(datadir_2, filename);
BOOST_CHECK(env_1_a != env_1_b);
BOOST_CHECK(env_2_a == env_2_b);
}
static std::vector<std::unique_ptr<WalletDatabase>> TestDatabases(const fs::path& path_root)
{
std::vector<std::unique_ptr<WalletDatabase>> dbs;
DatabaseOptions options;
DatabaseStatus status;
bilingual_str error;
#ifdef USE_BDB
dbs.emplace_back(MakeBerkeleyDatabase(path_root / "bdb", options, status, error));
// Needs BDB to make the DB to read
dbs.emplace_back(std::make_unique<BerkeleyRODatabase>(BDBDataFile(path_root / "bdb"), /*open=*/false));
#endif
#ifdef USE_SQLITE
dbs.emplace_back(MakeSQLiteDatabase(path_root / "sqlite", options, status, error));
#endif
dbs.emplace_back(CreateMockableWalletDatabase());
return dbs;
}
BOOST_AUTO_TEST_CASE(db_cursor_prefix_range_test)
{
// Test each supported db
for (const auto& database : TestDatabases(m_path_root)) {
std::vector<std::string> prefixes = {"", "FIRST", "SECOND", "P\xfe\xff", "P\xff\x01", "\xff\xff"};
2023-06-14 15:07:36 +01:00
std::unique_ptr<DatabaseBatch> handler = Assert(database)->MakeBatch();
if (dynamic_cast<BerkeleyRODatabase*>(database.get())) {
// For BerkeleyRO, open the file now. This must happen after BDB has written to the file
database->Open();
} else {
// Write elements to it if not berkeleyro
for (unsigned int i = 0; i < 10; i++) {
for (const auto& prefix : prefixes) {
BOOST_CHECK(handler->Write(std::make_pair(prefix, i), i));
}
}
}
// Now read all the items by prefix and verify that each element gets parsed correctly
for (const auto& prefix : prefixes) {
DataStream s_prefix;
s_prefix << prefix;
std::unique_ptr<DatabaseCursor> cursor = handler->GetNewPrefixCursor(s_prefix);
DataStream key;
DataStream value;
for (int i = 0; i < 10; i++) {
DatabaseCursor::Status status = cursor->Next(key, value);
2023-06-14 15:07:36 +01:00
BOOST_CHECK_EQUAL(status, DatabaseCursor::Status::MORE);
std::string key_back;
unsigned int i_back;
key >> key_back >> i_back;
BOOST_CHECK_EQUAL(key_back, prefix);
unsigned int value_back;
value >> value_back;
BOOST_CHECK_EQUAL(value_back, i_back);
}
// Let's now read it once more, it should return DONE
BOOST_CHECK(cursor->Next(key, value) == DatabaseCursor::Status::DONE);
}
handler.reset();
database->Close();
}
}
// Lower level DatabaseBase::GetNewPrefixCursor test, to cover cases that aren't
// covered in the higher level test above. The higher level test uses
// serialized strings which are prefixed with string length, so it doesn't test
// truly empty prefixes or prefixes that begin with \xff
BOOST_AUTO_TEST_CASE(db_cursor_prefix_byte_test)
{
const MockableData::value_type
e{StringData(""), StringData("e")},
p{StringData("prefix"), StringData("p")},
ps{StringData("prefixsuffix"), StringData("ps")},
f{StringData("\xff"), StringData("f")},
fs{StringData("\xffsuffix"), StringData("fs")},
ff{StringData("\xff\xff"), StringData("ff")},
ffs{StringData("\xff\xffsuffix"), StringData("ffs")};
for (const auto& database : TestDatabases(m_path_root)) {
std::unique_ptr<DatabaseBatch> batch = database->MakeBatch();
if (dynamic_cast<BerkeleyRODatabase*>(database.get())) {
// For BerkeleyRO, open the file now. This must happen after BDB has written to the file
database->Open();
} else {
// Write elements to it if not berkeleyro
for (const auto& [k, v] : {e, p, ps, f, fs, ff, ffs}) {
batch->Write(Span{k}, Span{v});
}
}
CheckPrefix(*batch, StringBytes(""), {e, p, ps, f, fs, ff, ffs});
CheckPrefix(*batch, StringBytes("prefix"), {p, ps});
CheckPrefix(*batch, StringBytes("\xff"), {f, fs, ff, ffs});
CheckPrefix(*batch, StringBytes("\xff\xff"), {ff, ffs});
batch.reset();
database->Close();
}
}
BOOST_AUTO_TEST_CASE(db_availability_after_write_error)
{
// Ensures the database remains accessible without deadlocking after a write error.
// To simulate the behavior, record overwrites are disallowed, and the test verifies
// that the database remains active after failing to store an existing record.
for (const auto& database : TestDatabases(m_path_root)) {
if (dynamic_cast<BerkeleyRODatabase*>(database.get())) {
// Skip this test if BerkeleyRO
continue;
}
// Write original record
std::unique_ptr<DatabaseBatch> batch = database->MakeBatch();
std::string key = "key";
std::string value = "value";
std::string value2 = "value_2";
BOOST_CHECK(batch->Write(key, value));
// Attempt to overwrite the record (expect failure)
BOOST_CHECK(!batch->Write(key, value2, /*fOverwrite=*/false));
// Successfully overwrite the record
BOOST_CHECK(batch->Write(key, value2, /*fOverwrite=*/true));
// Sanity-check; read and verify the overwritten value
std::string read_value;
BOOST_CHECK(batch->Read(key, read_value));
BOOST_CHECK_EQUAL(read_value, value2);
}
}
// Verify 'ErasePrefix' functionality using db keys similar to the ones used by the wallet.
// Keys are in the form of std::pair<TYPE, ENTRY_ID>
BOOST_AUTO_TEST_CASE(erase_prefix)
{
const std::string key = "key";
const std::string key2 = "key2";
const std::string value = "value";
const std::string value2 = "value_2";
auto make_key = [](std::string type, std::string id) { return std::make_pair(type, id); };
for (const auto& database : TestDatabases(m_path_root)) {
if (dynamic_cast<BerkeleyRODatabase*>(database.get())) {
// Skip this test if BerkeleyRO
continue;
}
std::unique_ptr<DatabaseBatch> batch = database->MakeBatch();
// Write two entries with the same key type prefix, a third one with a different prefix
// and a fourth one with the type-id values inverted
BOOST_CHECK(batch->Write(make_key(key, value), value));
BOOST_CHECK(batch->Write(make_key(key, value2), value2));
BOOST_CHECK(batch->Write(make_key(key2, value), value));
BOOST_CHECK(batch->Write(make_key(value, key), value));
// Erase the ones with the same prefix and verify result
BOOST_CHECK(batch->TxnBegin());
BOOST_CHECK(batch->ErasePrefix(DataStream() << key));
BOOST_CHECK(batch->TxnCommit());
BOOST_CHECK(!batch->Exists(make_key(key, value)));
BOOST_CHECK(!batch->Exists(make_key(key, value2)));
// Also verify that entries with a different prefix were not erased
BOOST_CHECK(batch->Exists(make_key(key2, value)));
BOOST_CHECK(batch->Exists(make_key(value, key)));
}
}
#ifdef USE_SQLITE
// Test-only statement execution error
constexpr int TEST_SQLITE_ERROR = -999;
class DbExecBlocker : public SQliteExecHandler
{
private:
SQliteExecHandler m_base_exec;
std::set<std::string> m_blocked_statements;
public:
DbExecBlocker(std::set<std::string> blocked_statements) : m_blocked_statements(blocked_statements) {}
int Exec(SQLiteDatabase& database, const std::string& statement) override {
if (m_blocked_statements.contains(statement)) return TEST_SQLITE_ERROR;
return m_base_exec.Exec(database, statement);
}
};
BOOST_AUTO_TEST_CASE(txn_close_failure_dangling_txn)
{
// Verifies that there is no active dangling, to-be-reversed db txn
// after the batch object that initiated it is destroyed.
DatabaseOptions options;
DatabaseStatus status;
bilingual_str error;
std::unique_ptr<SQLiteDatabase> database = MakeSQLiteDatabase(m_path_root / "sqlite", options, status, error);
std::string key = "key";
std::string value = "value";
std::unique_ptr<SQLiteBatch> batch = std::make_unique<SQLiteBatch>(*database);
BOOST_CHECK(batch->TxnBegin());
BOOST_CHECK(batch->Write(key, value));
// Set a handler to prevent txn abortion during destruction.
// Mimicking a db statement execution failure.
batch->SetExecHandler(std::make_unique<DbExecBlocker>(std::set<std::string>{"ROLLBACK TRANSACTION"}));
// Destroy batch
batch.reset();
// Ensure there is no dangling, to-be-reversed db txn
BOOST_CHECK(!database->HasActiveTxn());
// And, just as a sanity check; verify that new batchs only write what they suppose to write
// and nothing else.
std::string key2 = "key2";
std::unique_ptr<SQLiteBatch> batch2 = std::make_unique<SQLiteBatch>(*database);
BOOST_CHECK(batch2->Write(key2, value));
// The first key must not exist
BOOST_CHECK(!batch2->Exists(key));
}
BOOST_AUTO_TEST_CASE(concurrent_txn_dont_interfere)
{
std::string key = "key";
std::string value = "value";
std::string value2 = "value_2";
DatabaseOptions options;
DatabaseStatus status;
bilingual_str error;
const auto& database = MakeSQLiteDatabase(m_path_root / "sqlite", options, status, error);
std::unique_ptr<DatabaseBatch> handler = Assert(database)->MakeBatch();
// Verify concurrent db transactions does not interfere between each other.
// Start db txn, write key and check the key does exist within the db txn.
BOOST_CHECK(handler->TxnBegin());
BOOST_CHECK(handler->Write(key, value));
BOOST_CHECK(handler->Exists(key));
// But, the same key, does not exist in another handler
std::unique_ptr<DatabaseBatch> handler2 = Assert(database)->MakeBatch();
BOOST_CHECK(handler2->Exists(key));
// Attempt to commit the handler txn calling the handler2 methods.
// Which, must not be possible.
BOOST_CHECK(!handler2->TxnCommit());
BOOST_CHECK(!handler2->TxnAbort());
// Only the first handler can commit the changes.
BOOST_CHECK(handler->TxnCommit());
// And, once commit is completed, handler2 can read the record
std::string read_value;
BOOST_CHECK(handler2->Read(key, read_value));
BOOST_CHECK_EQUAL(read_value, value);
// Also, once txn is committed, single write statements are re-enabled.
// Which means that handler2 can read the record changes directly.
BOOST_CHECK(handler->Write(key, value2, /*fOverwrite=*/true));
BOOST_CHECK(handler2->Read(key, read_value));
BOOST_CHECK_EQUAL(read_value, value2);
}
#endif // USE_SQLITE
2018-09-14 21:10:20 -04:00
BOOST_AUTO_TEST_SUITE_END()
} // namespace wallet