diff --git a/src/wallet/sqlite.cpp b/src/wallet/sqlite.cpp index ea2644617..833c7cd42 100644 --- a/src/wallet/sqlite.cpp +++ b/src/wallet/sqlite.cpp @@ -1,160 +1,264 @@ // 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 #include +#include #include #include #include #include static const char *const DATABASE_FILENAME = "wallet.dat"; static Mutex g_sqlite_mutex; static int g_sqlite_count GUARDED_BY(g_sqlite_mutex) = 0; static void ErrorLogCallback(void *arg, int code, const char *msg) { // From sqlite3_config() documentation for the SQLITE_CONFIG_LOG option: // "The void pointer that is the second argument to SQLITE_CONFIG_LOG is // passed through as the first parameter to the application-defined logger // function whenever that function is invoked." // Assert that this is the case: assert(arg == nullptr); LogPrintf("SQLite Error. Code: %d. Message: %s\n", code, msg); } SQLiteDatabase::SQLiteDatabase(const fs::path &dir_path, const fs::path &file_path, bool mock) - // FIXME: uncomment m_mock when backporting commit a0de833 - : WalletDatabase(), /* m_mock(mock), */ m_dir_path(dir_path.string()), + : WalletDatabase(), m_mock(mock), m_dir_path(dir_path.string()), m_file_path(file_path.string()) { { LOCK(g_sqlite_mutex); LogPrintf("Using SQLite Version %s\n", SQLiteDatabaseVersion()); LogPrintf("Using wallet %s\n", m_dir_path); if (++g_sqlite_count == 1) { // Setup logging int ret = sqlite3_config(SQLITE_CONFIG_LOG, ErrorLogCallback, nullptr); if (ret != SQLITE_OK) { throw std::runtime_error( strprintf("SQLiteDatabase: Failed to setup error log: %s\n", sqlite3_errstr(ret))); } } // This is a no-op if sqlite3 is already initialized int ret = sqlite3_initialize(); if (ret != SQLITE_OK) { throw std::runtime_error( strprintf("SQLiteDatabase: Failed to initialize SQLite: %s\n", sqlite3_errstr(ret))); } } try { Open(); } catch (const std::runtime_error &) { // If open fails, cleanup this object and rethrow the exception Cleanup(); throw; } } SQLiteDatabase::~SQLiteDatabase() { Cleanup(); } void SQLiteDatabase::Cleanup() noexcept { Close(); LOCK(g_sqlite_mutex); if (--g_sqlite_count == 0) { int ret = sqlite3_shutdown(); if (ret != SQLITE_OK) { LogPrintf("SQLiteDatabase: Failed to shutdown SQLite: %s\n", sqlite3_errstr(ret)); } } } -void SQLiteDatabase::Open() {} +void SQLiteDatabase::Open() { + int flags = + SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE; + if (m_mock) { + // In memory database for mock db + flags |= SQLITE_OPEN_MEMORY; + } + + if (m_db == nullptr) { + TryCreateDirectories(m_dir_path); + int ret = sqlite3_open_v2(m_file_path.c_str(), &m_db, flags, nullptr); + if (ret != SQLITE_OK) { + throw std::runtime_error( + strprintf("SQLiteDatabase: Failed to open database: %s\n", + sqlite3_errstr(ret))); + } + } + + if (sqlite3_db_readonly(m_db, "main") != 0) { + throw std::runtime_error("SQLiteDatabase: Database opened in readonly " + "mode but read-write permissions are needed"); + } + + // Acquire an exclusive lock on the database + // First change the locking mode to exclusive + int ret = sqlite3_exec(m_db, "PRAGMA locking_mode = exclusive", nullptr, + nullptr, nullptr); + if (ret != SQLITE_OK) { + throw std::runtime_error( + strprintf("SQLiteDatabase: Unable to change database locking mode " + "to exclusive: %s\n", + sqlite3_errstr(ret))); + } + // Now begin a transaction to acquire the exclusive lock. This lock won't be + // released until we close because of the exclusive locking mode. + ret = sqlite3_exec(m_db, "BEGIN EXCLUSIVE TRANSACTION", nullptr, nullptr, + nullptr); + if (ret != SQLITE_OK) { + throw std::runtime_error( + "SQLiteDatabase: Unable to obtain an exclusive lock on the " + "database, is it being used by another bitcoind?\n"); + } + ret = sqlite3_exec(m_db, "COMMIT", nullptr, nullptr, nullptr); + if (ret != SQLITE_OK) { + throw std::runtime_error(strprintf( + "SQLiteDatabase: Unable to end exclusive lock transaction: %s\n", + sqlite3_errstr(ret))); + } + + // Enable fullfsync for the platforms that use it + ret = sqlite3_exec(m_db, "PRAGMA fullfsync = true", nullptr, nullptr, + nullptr); + if (ret != SQLITE_OK) { + throw std::runtime_error( + strprintf("SQLiteDatabase: Failed to enable fullfsync: %s\n", + sqlite3_errstr(ret))); + } + + // Make the table for our key-value pairs + // First check that the main table exists + sqlite3_stmt *check_main_stmt{nullptr}; + ret = sqlite3_prepare_v2( + m_db, + "SELECT name FROM sqlite_master WHERE type='table' AND name='main'", -1, + &check_main_stmt, nullptr); + if (ret != SQLITE_OK) { + throw std::runtime_error( + strprintf("SQLiteDatabase: Failed to prepare statement to check " + "table existence: %s\n", + sqlite3_errstr(ret))); + } + ret = sqlite3_step(check_main_stmt); + if (sqlite3_finalize(check_main_stmt) != SQLITE_OK) { + throw std::runtime_error( + strprintf("SQLiteDatabase: Failed to finalize statement checking " + "table existence: %s\n", + sqlite3_errstr(ret))); + } + bool table_exists; + if (ret == SQLITE_DONE) { + table_exists = false; + } else if (ret == SQLITE_ROW) { + table_exists = true; + } else { + throw std::runtime_error( + strprintf("SQLiteDatabase: Failed to execute statement to check " + "table existence: %s\n", + sqlite3_errstr(ret))); + } + + // Do the db setup things because the table doesn't exist only when we are + // creating a new wallet + if (!table_exists) { + ret = sqlite3_exec(m_db, + "CREATE TABLE main(key BLOB PRIMARY KEY NOT NULL, " + "value BLOB NOT NULL)", + nullptr, nullptr, nullptr); + if (ret != SQLITE_OK) { + throw std::runtime_error( + strprintf("SQLiteDatabase: Failed to create new database: %s\n", + sqlite3_errstr(ret))); + } + } +} bool SQLiteDatabase::Rewrite(const char *skip) { return false; } bool SQLiteDatabase::Backup(const std::string &dest) const { return false; } void SQLiteDatabase::Close() {} std::unique_ptr SQLiteDatabase::MakeBatch(bool flush_on_close) { return nullptr; } -// FIXME: uncomment m_database when backporting commit a0de833 -SQLiteBatch::SQLiteBatch( - SQLiteDatabase &database) /* : m_database(database) */ {} +SQLiteBatch::SQLiteBatch(SQLiteDatabase &database) : m_database(database) { + // Make sure we have a db handle + assert(m_database.m_db); +} void SQLiteBatch::Close() {} bool SQLiteBatch::ReadKey(CDataStream &&key, CDataStream &value) { return false; } bool SQLiteBatch::WriteKey(CDataStream &&key, CDataStream &&value, bool overwrite) { return false; } bool SQLiteBatch::EraseKey(CDataStream &&key) { return false; } bool SQLiteBatch::HasKey(CDataStream &&key) { return false; } bool SQLiteBatch::StartCursor() { return false; } bool SQLiteBatch::ReadAtCursor(CDataStream &key, CDataStream &value, bool &complete) { return false; } void SQLiteBatch::CloseCursor() {} bool SQLiteBatch::TxnBegin() { return false; } bool SQLiteBatch::TxnCommit() { return false; } bool SQLiteBatch::TxnAbort() { return false; } bool ExistsSQLiteDatabase(const fs::path &path) { return false; } std::unique_ptr MakeSQLiteDatabase(const fs::path &path, const DatabaseOptions &options, DatabaseStatus &status, bilingual_str &error) { return std::make_unique(path, path / DATABASE_FILENAME); } std::string SQLiteDatabaseVersion() { return std::string(sqlite3_libversion()); } diff --git a/src/wallet/sqlite.h b/src/wallet/sqlite.h index e883ccc2c..29e36244e 100644 --- a/src/wallet/sqlite.h +++ b/src/wallet/sqlite.h @@ -1,114 +1,112 @@ // 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_SQLITE_H #define BITCOIN_WALLET_SQLITE_H #include #include struct bilingual_str; class SQLiteDatabase; /** RAII class that provides access to a WalletDatabase */ class SQLiteBatch : public DatabaseBatch { private: - // FIXME: uncomment m_database when it is used in commit a0de833 - // SQLiteDatabase &m_database; + SQLiteDatabase &m_database; bool ReadKey(CDataStream &&key, CDataStream &value) override; bool WriteKey(CDataStream &&key, CDataStream &&value, bool overwrite = true) override; bool EraseKey(CDataStream &&key) override; bool HasKey(CDataStream &&key) override; public: explicit SQLiteBatch(SQLiteDatabase &database); ~SQLiteBatch() override { Close(); } /** No-op. See comment on SQLiteDatabase::Flush */ void Flush() override {} void Close() override; bool StartCursor() override; bool ReadAtCursor(CDataStream &key, CDataStream &value, bool &complete) override; void CloseCursor() override; bool TxnBegin() override; bool TxnCommit() override; bool TxnAbort() override; }; /** An instance of this class represents one SQLite3 database. */ class SQLiteDatabase : public WalletDatabase { private: - // FIXME: uncomment this line when m_mock is used in commit a0de833 - // const bool m_mock{false}; + const bool m_mock{false}; const std::string m_dir_path; const std::string m_file_path; void Cleanup() noexcept; public: SQLiteDatabase() = delete; /** Create DB handle to real database */ SQLiteDatabase(const fs::path &dir_path, const fs::path &file_path, bool mock = false); ~SQLiteDatabase(); /** Open the database if it is not already opened */ void Open() override; /** Close the database */ void Close() override; /* These functions are unused */ void AddRef() override { assert(false); } void RemoveRef() override { assert(false); } /** Rewrite the entire database on disk */ bool Rewrite(const char *skip = nullptr) override; /** Back up the entire database to a file. */ bool Backup(const std::string &dest) const override; /** * No-ops * * SQLite always flushes everything to the database file after each * transaction (each Read/Write/Erase that we do is its own transaction * unless we called TxnBegin) so there is no need to have Flush or Periodic * Flush. * * There is no DB env to reload, so ReloadDbEnv has nothing to do */ void Flush() override {} bool PeriodicFlush() override { return false; } void ReloadDbEnv() override {} void IncrementUpdateCounter() override { ++nUpdateCounter; } std::string Filename() override { return m_file_path; } /** Make a SQLiteBatch connected to this database */ std::unique_ptr MakeBatch(bool flush_on_close = true) override; sqlite3 *m_db{nullptr}; }; bool ExistsSQLiteDatabase(const fs::path &path); std::unique_ptr MakeSQLiteDatabase(const fs::path &path, const DatabaseOptions &options, DatabaseStatus &status, bilingual_str &error); std::string SQLiteDatabaseVersion(); #endif // BITCOIN_WALLET_SQLITE_H