Okay I'm sorry I can't seem to just let it go but here's another approach. It adds SaturatingLeftShift and CheckedLeftShift() util/overflow.h functionality (+tests, pinky promise), a _MiB string literal to allow remaining sizes to be converted to size_t bytes with concise notation and compile-time overflow guarantees (🤞). It also incorporates @ryanofsky's suggestion to just saturate -dbcache values instead of throwing. It's not necessary for my suggestion but I think it's a good approach and aligns well with the SaturatingLeftShift() helper fn.
<details>
<summary>git diff on 82706e217f</summary>
diff --git a/src/bitcoin-chainstate.cpp b/src/bitcoin-chainstate.cpp
index 82bda4b22b..51c482607a 100644
--- a/src/bitcoin-chainstate.cpp
+++ b/src/bitcoin-chainstate.cpp
@@ -26,7 +26,6 @@
#include <random.h>
#include <script/sigcache.h>
#include <util/chaintype.h>
-#include <util/byte_conversion.h>
#include <util/fs.h>
#include <util/signalinterrupt.h>
#include <util/task_runner.h>
@@ -124,7 +123,7 @@ int main(int argc, char* argv[])
util::SignalInterrupt interrupt;
ChainstateManager chainman{interrupt, chainman_opts, blockman_opts};
- kernel::CacheSizes cache_sizes{MiBToBytes(DEFAULT_KERNEL_CACHE)};
+ kernel::CacheSizes cache_sizes{DEFAULT_KERNEL_CACHE};
node::ChainstateLoadOptions options;
auto [status, error] = node::LoadChainstate(chainman, cache_sizes, options);
if (status != node::ChainstateLoadStatus::SUCCESS) {
diff --git a/src/init.cpp b/src/init.cpp
index 1ae968760d..847facad87 100644
--- a/src/init.cpp
+++ b/src/init.cpp
@@ -489,7 +489,7 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc)
argsman.AddArg("-conf=<file>", strprintf("Specify path to read-only configuration file. Relative paths will be prefixed by datadir location (only useable from command line, not configuration file) (default: %s)", BITCOIN_CONF_FILENAME), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-datadir=<dir>", "Specify data directory", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::OPTIONS);
argsman.AddArg("-dbbatchsize", strprintf("Maximum database write batch size in bytes (default: %u)", nDefaultDbBatchSize), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::OPTIONS);
- argsman.AddArg("-dbcache=<n>", strprintf("Maximum database cache size <n> MiB (minimum %d, default: %d). Make sure you have enough RAM. In addition, unused memory allocated to the mempool is shared with this cache (see -maxmempool).", MIN_DB_CACHE, DEFAULT_DB_CACHE), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
+ argsman.AddArg("-dbcache=<n>", strprintf("Maximum database cache size <n> MiB (minimum %d, default: %d). Make sure you have enough RAM. In addition, unused memory allocated to the mempool is shared with this cache (see -maxmempool).", MIN_DB_CACHE >> 20, DEFAULT_DB_CACHE >> 20), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-includeconf=<file>", "Specify additional configuration file, relative to the -datadir path (only useable from configuration file, not command line)", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-allowignoredconf", strprintf("For backwards compatibility, treat an unused %s file in the datadir as a warning, not an error.", BITCOIN_CONF_FILENAME), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-loadblock=<file>", "Imports blocks from external file on startup", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
@@ -1179,7 +1179,7 @@ static ChainstateLoadResult InitAndLoadChainstate(
NodeContext& node,
bool do_reindex,
const bool do_reindex_chainstate,
- CacheSizes& cache_sizes,
+ const CacheSizes& cache_sizes,
const ArgsManager& args)
{
const CChainParams& chainparams = Params();
@@ -1605,11 +1605,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
ReadNotificationArgs(args, kernel_notifications);
// cache size calculations
- const auto cache_sizes = CalculateCacheSizes(args, g_enabled_filter_types.size());
- if (!cache_sizes) {
- return InitError(_("Failed to calculate cache sizes. Try lowering the -dbcache value."));
- }
- auto [index_cache_sizes, kernel_cache_sizes] = *cache_sizes;
+ const auto [index_cache_sizes, kernel_cache_sizes] = CalculateCacheSizes(args, g_enabled_filter_types.size());
LogInfo("Cache configuration:");
LogInfo("* Using %.1f MiB for block index database", kernel_cache_sizes.block_tree_db * (1.0 / 1024 / 1024));
diff --git a/src/kernel/caches.h b/src/kernel/caches.h
index 9ee5a31e0a..4414d1a123 100644
--- a/src/kernel/caches.h
+++ b/src/kernel/caches.h
@@ -5,16 +5,16 @@
#ifndef BITCOIN_KERNEL_CACHES_H
#define BITCOIN_KERNEL_CACHES_H
-#include <util/byte_conversion.h>
+#include <util/storage.h>
#include <algorithm>
-//! Suggested default amount of cache reserved for the kernel (MiB)
-static constexpr int64_t DEFAULT_KERNEL_CACHE{450};
+//! Suggested default amount of cache reserved for the kernel (bytes)
+static constexpr size_t DEFAULT_KERNEL_CACHE{450_MiB};
//! Max memory allocated to block tree DB specific cache (bytes)
-static constexpr size_t MAX_BLOCK_DB_CACHE{MiBToBytes(2)};
+static constexpr size_t MAX_BLOCK_DB_CACHE{2_MiB};
//! Max memory allocated to coin DB specific cache (bytes)
-static constexpr size_t MAX_COINS_DB_CACHE{MiBToBytes(8)};
+static constexpr size_t MAX_COINS_DB_CACHE{8_MiB};
namespace kernel {
struct CacheSizes {
diff --git a/src/node/caches.cpp b/src/node/caches.cpp
index 41a8971665..94ad5bbe61 100644
--- a/src/node/caches.cpp
+++ b/src/node/caches.cpp
@@ -8,34 +8,27 @@
#include <index/txindex.h>
#include <kernel/caches.h>
#include <logging.h>
-#include <util/byte_conversion.h>
+#include <util/overflow.h>
+#include <util/storage.h>
#include <algorithm>
-#include <optional>
#include <stdexcept>
#include <string>
// Unlike for the UTXO database, for the txindex scenario the leveldb cache make
// a meaningful difference: [#8273 (comment)](/bitcoin-bitcoin/8273/#issuecomment-229601991)
//! Max memory allocated to tx index DB specific cache in bytes.
-static constexpr size_t MAX_TX_INDEX_CACHE{MiBToBytes(1024)};
+static constexpr size_t MAX_TX_INDEX_CACHE{1024_MiB};
//! Max memory allocated to all block filter index caches combined in bytes.
-static constexpr size_t MAX_FILTER_INDEX_CACHE{MiBToBytes(1024)};
+static constexpr size_t MAX_FILTER_INDEX_CACHE{1024_MiB};
namespace node {
-std::optional<CacheSizes> CalculateCacheSizes(const ArgsManager& args, size_t n_indexes)
+CacheSizes CalculateCacheSizes(const ArgsManager& args, size_t n_indexes)
{
- int64_t db_cache = args.GetIntArg("-dbcache", DEFAULT_DB_CACHE);
-
- // negative values are permitted, but interpreted as zero.
- db_cache = std::max(int64_t{0}, db_cache);
-
- size_t total_cache = 0;
- try {
- total_cache = std::max(MiBToBytes(db_cache), MiBToBytes(MIN_DB_CACHE));
- } catch (const std::out_of_range&) {
- LogError("Cannot allocate more than %d MiB in total for db caches.", std::numeric_limits<size_t>::max() >> 20);
- return std::nullopt;
+ // Total cache size in MiB, floored by MIN_DB_CACHE and capped by max size_t value
+ size_t total_cache{DEFAULT_DB_CACHE};
+ if (auto db_cache = args.GetIntArg("-dbcache")) {
+ total_cache = std::max(MIN_DB_CACHE, SaturatingLeftShift<size_t>(*db_cache, 20));
}
IndexCacheSizes index_sizes;
@@ -47,6 +40,6 @@ std::optional<CacheSizes> CalculateCacheSizes(const ArgsManager& args, size_t n_
index_sizes.filter_index = max_cache / n_indexes;
total_cache -= index_sizes.filter_index * n_indexes;
}
- return {{index_sizes, kernel::CacheSizes{total_cache}}};
+ return {index_sizes, kernel::CacheSizes{total_cache}};
}
} // namespace node
diff --git a/src/node/caches.h b/src/node/caches.h
index b4f2e3d516..aab7871e93 100644
--- a/src/node/caches.h
+++ b/src/node/caches.h
@@ -6,17 +6,16 @@
#define BITCOIN_NODE_CACHES_H
#include <kernel/caches.h>
-#include <util/byte_conversion.h>
+#include <util/storage.h>
#include <cstddef>
-#include <optional>
class ArgsManager;
-//! min. -dbcache (MiB)
-static constexpr int64_t MIN_DB_CACHE{4};
-//! -dbcache default (MiB)
-static constexpr int64_t DEFAULT_DB_CACHE{DEFAULT_KERNEL_CACHE};
+//! min. -dbcache (bytes)
+static constexpr size_t MIN_DB_CACHE{4_MiB};
+//! -dbcache default (bytes)
+static constexpr size_t DEFAULT_DB_CACHE{DEFAULT_KERNEL_CACHE};
namespace node {
struct IndexCacheSizes {
@@ -27,7 +26,7 @@ struct CacheSizes {
IndexCacheSizes index;
kernel::CacheSizes kernel;
};
-std::optional<CacheSizes> CalculateCacheSizes(const ArgsManager& args, size_t n_indexes = 0);
+CacheSizes CalculateCacheSizes(const ArgsManager& args, size_t n_indexes = 0);
} // namespace node
#endif // BITCOIN_NODE_CACHES_H
diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp
index 76b7852109..2cea1bf6ab 100644
--- a/src/qt/optionsdialog.cpp
+++ b/src/qt/optionsdialog.cpp
@@ -95,7 +95,7 @@ OptionsDialog::OptionsDialog(QWidget* parent, bool enableWallet)
ui->verticalLayout->setStretchFactor(ui->tabWidget, 1);
/* Main elements init */
- ui->databaseCache->setRange(MIN_DB_CACHE, std::numeric_limits<int>::max());
+ ui->databaseCache->setRange(MIN_DB_CACHE >> 20, std::numeric_limits<int>::max());
ui->threadsScriptVerif->setMinimum(-GetNumCores());
ui->threadsScriptVerif->setMaximum(MAX_SCRIPTCHECK_THREADS);
ui->pruneWarning->setVisible(false);
diff --git a/src/qt/optionsmodel.cpp b/src/qt/optionsmodel.cpp
index 2c6f13dc6d..5ff22b0f4c 100644
--- a/src/qt/optionsmodel.cpp
+++ b/src/qt/optionsmodel.cpp
@@ -470,7 +470,7 @@ QVariant OptionsModel::getOption(OptionID option, const std::string& suffix) con
suffix.empty() ? getOption(option, "-prev") :
DEFAULT_PRUNE_TARGET_GB;
case DatabaseCache:
- return qlonglong(SettingToInt(setting(), DEFAULT_DB_CACHE));
+ return qlonglong(SettingToInt(setting(), DEFAULT_DB_CACHE >> 20));
case ThreadsScriptVerif:
return qlonglong(SettingToInt(setting(), DEFAULT_SCRIPTCHECK_THREADS));
case Listen:
@@ -733,7 +733,7 @@ void OptionsModel::checkAndMigrate()
// see [#8273](/bitcoin-bitcoin/8273/)
// force people to upgrade to the new value if they are using 100MB
if (settingsVersion < 130000 && settings.contains("nDatabaseCache") && settings.value("nDatabaseCache").toLongLong() == 100)
- settings.setValue("nDatabaseCache", (qint64)DEFAULT_DB_CACHE);
+ settings.setValue("nDatabaseCache", (qint64)DEFAULT_DB_CACHE >> 20);
settings.setValue(strSettingsVersionKey, CLIENT_VERSION);
}
diff --git a/src/test/util/setup_common.h b/src/test/util/setup_common.h
index 7d7c7b7cd4..33ad258457 100644
--- a/src/test/util/setup_common.h
+++ b/src/test/util/setup_common.h
@@ -104,7 +104,7 @@ struct BasicTestingSetup {
* initialization behaviour.
*/
struct ChainTestingSetup : public BasicTestingSetup {
- kernel::CacheSizes m_kernel_cache_sizes{node::CalculateCacheSizes(m_args).value().kernel};
+ kernel::CacheSizes m_kernel_cache_sizes{node::CalculateCacheSizes(m_args).kernel};
bool m_coins_db_in_memory{true};
bool m_block_tree_db_in_memory{true};
std::function<void()> m_make_chainman{};
diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp
index e8e0abbda6..dbe1d76ca8 100644
--- a/src/test/util_tests.cpp
+++ b/src/test/util_tests.cpp
@@ -13,12 +13,12 @@
#include <test/util/setup_common.h>
#include <uint256.h>
#include <util/bitdeque.h>
-#include <util/byte_conversion.h>
#include <util/fs.h>
#include <util/fs_helpers.h>
#include <util/moneystr.h>
#include <util/overflow.h>
#include <util/readwritefile.h>
+#include <util/storage.h>
#include <util/strencodings.h>
#include <util/string.h>
#include <util/time.h>
@@ -141,19 +141,6 @@ BOOST_AUTO_TEST_CASE(util_criticalsection)
} while(0);
}
-BOOST_AUTO_TEST_CASE(byte_conversion)
-{
- // maximum allowed value in MiB
- const int64_t max_cache = std::numeric_limits<size_t>::max() >> 20;
-
- BOOST_CHECK_EXCEPTION(MiBToBytes(-1), std::out_of_range, HasReason("Value may not be negative."));
- BOOST_CHECK_EXCEPTION(MiBToBytes(std::numeric_limits<int64_t>::max()), std::out_of_range, HasReason("Conversion to bytes of"));
- BOOST_CHECK_EXCEPTION(MiBToBytes(max_cache + 1), std::out_of_range, HasReason("Conversion to bytes of"));
-
- BOOST_CHECK_EQUAL(MiBToBytes(0), 0);
- BOOST_CHECK_EQUAL(MiBToBytes(max_cache), max_cache << 20);
-}
-
constexpr char HEX_PARSE_INPUT[] = "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f";
constexpr uint8_t HEX_PARSE_OUTPUT[] = {
0x04, 0x67, 0x8a, 0xfd, 0xb0, 0xfe, 0x55, 0x48, 0x27, 0x19, 0x67, 0xf1, 0xa6, 0x71, 0x30, 0xb7,
@@ -1892,4 +1879,63 @@ BOOST_AUTO_TEST_CASE(clearshrink_test)
}
}
+template <typename T>
+void TestCheckedLeftShift()
+{
+ BOOST_TEST_CONTEXT("Testing CheckedLeftShift with " << typeid(T).name()) {
+ // Basic operations
+ BOOST_CHECK_EQUAL(CheckedLeftShift<T>(0, 1), 0);
+ BOOST_CHECK_EQUAL(CheckedLeftShift<T>(1, 1), 2);
+ BOOST_CHECK_EQUAL(CheckedLeftShift<T>(2, 2), 8);
+
+ // Negative input
+ BOOST_CHECK(!CheckedLeftShift<T>(-1, 1));
+
+ // Overflow cases
+ BOOST_CHECK(!CheckedLeftShift<T>((std::numeric_limits<T>::max() >> 1) + 1, 1));
+ BOOST_CHECK(!CheckedLeftShift<T>(std::numeric_limits<T>::max(), 1));
+ }
+}
+
+template <typename T>
+void TestSaturatingLeftShift()
+{
+ BOOST_TEST_CONTEXT("Testing SaturatingLeftShift with " << typeid(T).name()) {
+ // Basic operations
+ BOOST_CHECK_EQUAL(SaturatingLeftShift<T>(0, 1), 0);
+ BOOST_CHECK_EQUAL(SaturatingLeftShift<T>(1, 1), 2);
+ BOOST_CHECK_EQUAL(SaturatingLeftShift<T>(2, 2), 8);
+
+ // Negative input
+ BOOST_CHECK_EQUAL(SaturatingLeftShift<T>(-1, 1), 0);
+
+ // Saturation cases
+ BOOST_CHECK_EQUAL(SaturatingLeftShift<T>((std::numeric_limits<T>::max() >> 1) + 1, 1), std::numeric_limits<T>::max());
+ BOOST_CHECK_EQUAL(SaturatingLeftShift<T>(std::numeric_limits<T>::max(), 1), std::numeric_limits<T>::max());
+ }
+}
+
+BOOST_AUTO_TEST_CASE(checked_left_shift_test)
+{
+ TestCheckedLeftShift<uint8_t>();
+ TestCheckedLeftShift<size_t>();
+ TestCheckedLeftShift<uint64_t>();
+}
+
+BOOST_AUTO_TEST_CASE(saturating_left_shift_test)
+{
+ TestSaturatingLeftShift<uint8_t>();
+ TestSaturatingLeftShift<size_t>();
+ TestSaturatingLeftShift<uint64_t>();
+}
+
+BOOST_AUTO_TEST_CASE(mib_string_literal_test)
+{
+ BOOST_CHECK_EQUAL(0_MiB, 0);
+ BOOST_CHECK_EQUAL(1_MiB, 1024 * 1024);
+
+ constexpr size_t max_safe_mib = std::numeric_limits<size_t>::max() / (1024 * 1024);
+ BOOST_CHECK_EQUAL(max_safe_mib * (1024 * 1024), max_safe_mib * 1_MiB);
+}
+
BOOST_AUTO_TEST_SUITE_END()
diff --git a/src/util/byte_conversion.h b/src/util/byte_conversion.h
deleted file mode 100644
index bbdad04bff..0000000000
--- a/src/util/byte_conversion.h
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (c) 2025-present 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_UTIL_BYTE_CONVERSION_H
-#define BITCOIN_UTIL_BYTE_CONVERSION_H
-
-#include <tinyformat.h>
-
-#include <cstdint>
-#include <limits>
-#include <stdexcept>
-
-//! Guard against truncation of values before converting.
-constexpr size_t MiBToBytes(int64_t mib)
-{
- if (mib < 0) {
- throw std::out_of_range("Value may not be negative.");
- }
- if (static_cast<uint64_t>(mib) > std::numeric_limits<size_t>::max() >> 20) {
- throw std::out_of_range(strprintf("Conversion to bytes of %d does not fit into size_t with maximum value %d.", mib, std::numeric_limits<size_t>::max()));
- }
- return static_cast<size_t>(mib) << 20;
-}
-
-#endif // BITCOIN_UTIL_BYTE_CONVERSION_H
diff --git a/src/util/overflow.h b/src/util/overflow.h
index 7e0cce6c27..bb5277db28 100644
--- a/src/util/overflow.h
+++ b/src/util/overflow.h
@@ -47,4 +47,42 @@ template <class T>
return i + j;
}
+/**
+ * [@brief](/bitcoin-bitcoin/contributor/brief/) Left bit shift with overflow checking.
+ * [@param](/bitcoin-bitcoin/contributor/param/) i The input value to be left shifted.
+ * [@param](/bitcoin-bitcoin/contributor/param/) shift The number of bits to left shift.
+ * [@return](/bitcoin-bitcoin/contributor/return/) The result of the left shift, or std::nullopt in case of
+ * overflow or negative input value.
+ */
+template <class Output, class Input>
+constexpr std::optional<Output> CheckedLeftShift(Input i, unsigned shift) noexcept
+ requires std::is_unsigned_v<Output>
+{
+ if constexpr (std::is_signed_v<Input>) {
+ if (i < 0) return std::nullopt;
+ }
+ if (std::make_unsigned_t<Input>(i) > (std::numeric_limits<Output>::max() >> shift)) {
+ return std::nullopt;
+ }
+ return i << shift;
+}
+
+/**
+ * [@brief](/bitcoin-bitcoin/contributor/brief/) Left bit shift with safe minimum and maximum values.
+ * [@param](/bitcoin-bitcoin/contributor/param/) i The input value to be left shifted.
+ * [@param](/bitcoin-bitcoin/contributor/param/) shift The number of bits to left shift.
+ * [@return](/bitcoin-bitcoin/contributor/return/) The result of the left shift, with the return value clamped
+ * between zero and the maximum Output value if overflow occurs.
+ */
+template <class Output, class Input>
+constexpr Output SaturatingLeftShift(Input i, unsigned shift) noexcept
+ requires std::is_unsigned_v<Output>
+{
+ auto default_value{std::numeric_limits<Output>::max()};
+ if constexpr (std::is_signed_v<Input>) {
+ if (i < 0) default_value = 0;
+ }
+ return CheckedLeftShift<Output>(i, shift).value_or(default_value);
+}
+
#endif // BITCOIN_UTIL_OVERFLOW_H
diff --git a/src/util/storage.h b/src/util/storage.h
new file mode 100644
index 0000000000..042875510b
--- /dev/null
+++ b/src/util/storage.h
@@ -0,0 +1,24 @@
+// Copyright (c) 2025-present 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_UTIL_STORAGE_H
+#define BITCOIN_UTIL_STORAGE_H
+
+#include <util/overflow.h>
+
+#include <cstdint>
+#include <limits>
+#include <stdexcept>
+
+//! Overflow-safe conversion of MiB to bytes.
+constexpr size_t operator"" _MiB(unsigned long long mebibytes)
+{
+ auto bytes{CheckedLeftShift<size_t>(static_cast<size_t>(mebibytes), 20)};
+ if (!bytes) {
+ throw std::overflow_error("mebibytes cannot be converted to bytes");
+ }
+ return *bytes;
+}
+
+#endif // BITCOIN_UTIL_STORAGE_H
</details>