From bf1843b6ca004b2522b63865a7591d2e32bff480 Mon Sep 17 00:00:00 2001 From: josibake Date: Thu, 19 Sep 2024 11:29:37 +0200 Subject: [PATCH] add read only option for mdbx --- src/dbwrapper.cpp | 17 ++++++++++++----- src/dbwrapper.h | 5 +++++ src/kernel/bitcoinkernel.cpp | 8 ++++++-- src/kernel/bitcoinkernel.h | 3 ++- src/node/chainstate.cpp | 4 +++- src/node/chainstate.h | 3 +++ src/test/validation_chainstate_tests.cpp | 2 +- src/test/validation_chainstatemanager_tests.cpp | 4 ++-- src/validation.cpp | 2 ++ src/validation.h | 1 + 10 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/dbwrapper.cpp b/src/dbwrapper.cpp index 94c4d1b655324..221f6bf323e47 100644 --- a/src/dbwrapper.cpp +++ b/src/dbwrapper.cpp @@ -420,14 +420,14 @@ MDBXWrapper::MDBXWrapper(const DBParams& params) : CDBWrapperBase(params), m_db_context{std::make_unique()} { - if (params.wipe_data) { + if (params.wipe_data && !params.read_only) { LogInfo("Wiping MDBX in %s\n", fs::PathToString(params.path)); DestroyDB(fs::PathToString(params.path)); } TryCreateDirectories(params.path); - LogPrintf("Opening MDBX in %s\n", fs::PathToString(params.path)); + LogPrintf("Opening MDBX in %s (read-only: %s)\n", fs::PathToString(params.path), params.read_only ? "yes" : "no"); DBContext().create_params.geometry.pagesize = 16384; @@ -436,6 +436,11 @@ MDBXWrapper::MDBXWrapper(const DBParams& params) DBContext().operate_params.options.no_sticky_threads = true; DBContext().operate_params.durability = mdbx::env::whole_fragile; + // Set read-only mode if specified + if (params.read_only) { + DBContext().operate_params.mode = mdbx::env::mode::readonly; + } + // initialize the mdbx environment. DBContext().env = mdbx::env_managed(params.path, DBContext().create_params, DBContext().operate_params); @@ -443,10 +448,12 @@ MDBXWrapper::MDBXWrapper(const DBParams& params) DBContext().map = txn.open_map(nullptr, mdbx::key_mode::usual, mdbx::value_mode::single); txn.commit(); - if (params.obfuscate && WriteObfuscateKeyIfNotExists()){ - LogInfo("Wrote new obfuscate key for %s: %s\n", fs::PathToString(params.path), HexStr(obfuscate_key)); + if (!params.read_only) { + if (params.obfuscate && WriteObfuscateKeyIfNotExists()){ + LogInfo("Wrote new obfuscate key for %s: %s\n", fs::PathToString(params.path), HexStr(obfuscate_key)); + } + LogInfo("Using obfuscation key for %s: %s\n", fs::PathToString(params.path), HexStr(GetObfuscateKey())); } - LogInfo("Using obfuscation key for %s: %s\n", fs::PathToString(params.path), HexStr(GetObfuscateKey())); } MDBXWrapper::~MDBXWrapper() = default; diff --git a/src/dbwrapper.h b/src/dbwrapper.h index 20944b8b7f5b5..a197749ae41ce 100644 --- a/src/dbwrapper.h +++ b/src/dbwrapper.h @@ -42,6 +42,11 @@ struct DBParams { //! If true, store data obfuscated via simple XOR. If false, XOR with a //! zero'd byte array. bool obfuscate = false; + //! Open the database in read-only mode, e.g., from a process outside bitcoind + //! Note: leveldb does not support multi-process readers, so it might make + //! more sense to have this be a DBOptions flag, since it is implementation + //! specific? + bool read_only = false; //! Passed-through options. DBOptions options{}; }; diff --git a/src/kernel/bitcoinkernel.cpp b/src/kernel/bitcoinkernel.cpp index 1bc2079ff7825..0e4fedd798dd6 100644 --- a/src/kernel/bitcoinkernel.cpp +++ b/src/kernel/bitcoinkernel.cpp @@ -1019,6 +1019,10 @@ void kernel_chainstate_load_options_set( chainstate_load_opts->coins_db_in_memory = value; return; } + case kernel_ChainstateLoadOptionType::kernel_READONLY: { + chainstate_load_opts->read_only = value; + return; + } } // no default case, so the compiler can warn about missing cases assert(false); } @@ -1384,7 +1388,7 @@ bool kernel_chainstate_manager_process_transaction( bool kernel_chainstate_manager_process_block( const kernel_Context* context_, kernel_ChainstateManager* chainman_, - kernel_Block* block_, + kernel_Block* block_, kernel_ProcessBlockStatus* status) { auto& chainman{*cast_chainstate_manager(chainman_)}; @@ -1565,7 +1569,7 @@ kernel_BlockHeader* kernel_get_block_header(kernel_Block* block_) { auto block{cast_cblocksharedpointer(block_)}; return reinterpret_cast(new CBlockHeader{(*block)->GetBlockHeader()}); -} +} kernel_BlockHeader* kernel_block_header_create(const unsigned char* raw_block_header, size_t raw_block_header_len) { diff --git a/src/kernel/bitcoinkernel.h b/src/kernel/bitcoinkernel.h index ec9c980f6bc88..68e29227fead7 100644 --- a/src/kernel/bitcoinkernel.h +++ b/src/kernel/bitcoinkernel.h @@ -340,6 +340,7 @@ typedef enum { kernel_WIPE_CHAINSTATE_DB_CHAINSTATE_LOAD_OPTION, //! Set the wipe chainstate option, default is false. kernel_BLOCK_TREE_DB_IN_MEMORY_CHAINSTATE_LOAD_OPTION, //! Set the block tree db in memory option, default is false. kernel_CHAINSTATE_DB_IN_MEMORY_CHAINSTATE_LOAD_OPTION, //! Set the coins db in memory option, default is false. + kernel_READONLY, //! Open the block tree db and coins db in read-only mode, e.g., from a process outside bitcoind } kernel_ChainstateLoadOptionType; /** @@ -1129,7 +1130,7 @@ kernel_BlockUndo* BITCOINKERNEL_WARN_UNUSED_RESULT kernel_read_block_undo_from_d /** * @brief Validates a passed in block header and on success adds it to the header chain. - * + * * @param[in] chainman Non-null. * @param[in] header Non-null, the header to be validated. * @return True if the header was successfully validated. diff --git a/src/node/chainstate.cpp b/src/node/chainstate.cpp index d7e6176be1e96..e439fe7d2ef52 100644 --- a/src/node/chainstate.cpp +++ b/src/node/chainstate.cpp @@ -46,6 +46,7 @@ static ChainstateLoadResult CompleteChainstateInitialization( .cache_bytes = static_cast(cache_sizes.block_tree_db), .memory_only = options.block_tree_db_in_memory, .wipe_data = options.wipe_block_tree_db, + .read_only = options.read_only, .options = chainman.m_options.block_tree_db}); if (options.wipe_block_tree_db) { @@ -110,7 +111,8 @@ static ChainstateLoadResult CompleteChainstateInitialization( chainstate->InitCoinsDB( /*cache_size_bytes=*/chainman.m_total_coinsdb_cache * init_cache_fraction, /*in_memory=*/options.coins_db_in_memory, - /*should_wipe=*/options.wipe_chainstate_db); + /*should_wipe=*/options.wipe_chainstate_db, + /*read_only=*/options.read_only); if (options.coins_error_cb) { chainstate->CoinsErrorCatcher().AddReadErrCallback(options.coins_error_cb); diff --git a/src/node/chainstate.h b/src/node/chainstate.h index bb0c4f2b87e86..c5d6fde8c8510 100644 --- a/src/node/chainstate.h +++ b/src/node/chainstate.h @@ -30,6 +30,9 @@ struct ChainstateLoadOptions { // will cause the chainstate database to be rebuilt starting from genesis. bool wipe_chainstate_db{false}; bool prune{false}; + // Open the block_tree and coins_db databases in read only mode, e.g., from a + // process other than bitcoind + bool read_only{false}; //! Setting require_full_verification to true will require all checks at //! check_level (below) to succeed for loading to succeed. Setting it to //! false will skip checks if cache is not big enough to run them, so may be diff --git a/src/test/validation_chainstate_tests.cpp b/src/test/validation_chainstate_tests.cpp index 30c5982b17d0b..9fbb201e49a09 100644 --- a/src/test/validation_chainstate_tests.cpp +++ b/src/test/validation_chainstate_tests.cpp @@ -29,7 +29,7 @@ BOOST_AUTO_TEST_CASE(validation_chainstate_resize_caches) CTxMemPool& mempool = *Assert(m_node.mempool); Chainstate& c1 = WITH_LOCK(cs_main, return manager.InitializeChainstate(&mempool)); c1.InitCoinsDB( - /*cache_size_bytes=*/1 << 23, /*in_memory=*/true, /*should_wipe=*/false); + /*cache_size_bytes=*/1 << 23, /*in_memory=*/true, /*should_wipe=*/false, /*read_only=*/false); WITH_LOCK(::cs_main, c1.InitCoinsCache(1 << 23)); BOOST_REQUIRE(c1.LoadGenesisBlock()); // Need at least one block loaded to be able to flush caches diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp index b4fcfbd85379c..07a1142bf9cc2 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -72,7 +72,7 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager, TestChain100Setup) Chainstate& c2 = WITH_LOCK(::cs_main, return manager.ActivateExistingSnapshot(snapshot_blockhash)); chainstates.push_back(&c2); c2.InitCoinsDB( - /*cache_size_bytes=*/1 << 23, /*in_memory=*/true, /*should_wipe=*/false); + /*cache_size_bytes=*/1 << 23, /*in_memory=*/true, /*should_wipe=*/false, /*read_only=*/false); { LOCK(::cs_main); c2.InitCoinsCache(1 << 23); @@ -138,7 +138,7 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_rebalance_caches, TestChain100Setup) Chainstate& c2 = WITH_LOCK(cs_main, return manager.ActivateExistingSnapshot(*snapshot_base->phashBlock)); chainstates.push_back(&c2); c2.InitCoinsDB( - /*cache_size_bytes=*/1 << 23, /*in_memory=*/true, /*should_wipe=*/false); + /*cache_size_bytes=*/1 << 23, /*in_memory=*/true, /*should_wipe=*/false, /*read_only=*/false); // Reset IBD state so IsInitialBlockDownload() returns true and causes // MaybeRebalancesCaches() to prioritize the snapshot chainstate, giving it diff --git a/src/validation.cpp b/src/validation.cpp index ccc84bc5fee39..69af31d049c9c 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1961,6 +1961,7 @@ void Chainstate::InitCoinsDB( size_t cache_size_bytes, bool in_memory, bool should_wipe, + bool read_only, fs::path leveldb_name) { if (m_from_snapshot_blockhash) { @@ -1974,6 +1975,7 @@ void Chainstate::InitCoinsDB( .memory_only = in_memory, .wipe_data = should_wipe, .obfuscate = true, + .read_only = read_only, .options = m_chainman.m_options.coins_db}, m_chainman.m_options.coins_view); } diff --git a/src/validation.h b/src/validation.h index 6bc8a14de95a6..108e41971f484 100644 --- a/src/validation.h +++ b/src/validation.h @@ -574,6 +574,7 @@ class Chainstate size_t cache_size_bytes, bool in_memory, bool should_wipe, + bool read_only, fs::path leveldb_name = "chainstate"); //! Initialize the in-memory coins cache (to be done after the health of the on-disk database