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.
0diff --git a/src/bitcoin-chainstate.cpp b/src/bitcoin-chainstate.cpp
1index 82bda4b22b..51c482607a 100644
2--- a/src/bitcoin-chainstate.cpp
3+++ b/src/bitcoin-chainstate.cpp
4@@ -26,7 +26,6 @@
5 #include <random.h>
6 #include <script/sigcache.h>
7 #include <util/chaintype.h>
8-#include <util/byte_conversion.h>
9 #include <util/fs.h>
10 #include <util/signalinterrupt.h>
11 #include <util/task_runner.h>
12@@ -124,7 +123,7 @@ int main(int argc, char* argv[])
13 util::SignalInterrupt interrupt;
14 ChainstateManager chainman{interrupt, chainman_opts, blockman_opts};
15
16- kernel::CacheSizes cache_sizes{MiBToBytes(DEFAULT_KERNEL_CACHE)};
17+ kernel::CacheSizes cache_sizes{DEFAULT_KERNEL_CACHE};
18 node::ChainstateLoadOptions options;
19 auto [status, error] = node::LoadChainstate(chainman, cache_sizes, options);
20 if (status != node::ChainstateLoadStatus::SUCCESS) {
21diff --git a/src/init.cpp b/src/init.cpp
22index 1ae968760d..847facad87 100644
23--- a/src/init.cpp
24+++ b/src/init.cpp
25@@ -489,7 +489,7 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc)
26 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);
27 argsman.AddArg("-datadir=<dir>", "Specify data directory", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::OPTIONS);
28 argsman.AddArg("-dbbatchsize", strprintf("Maximum database write batch size in bytes (default: %u)", nDefaultDbBatchSize), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::OPTIONS);
29- 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);
30+ 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);
31 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);
32 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);
33 argsman.AddArg("-loadblock=<file>", "Imports blocks from external file on startup", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
34@@ -1179,7 +1179,7 @@ static ChainstateLoadResult InitAndLoadChainstate(
35 NodeContext& node,
36 bool do_reindex,
37 const bool do_reindex_chainstate,
38- CacheSizes& cache_sizes,
39+ const CacheSizes& cache_sizes,
40 const ArgsManager& args)
41 {
42 const CChainParams& chainparams = Params();
43@@ -1605,11 +1605,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
44 ReadNotificationArgs(args, kernel_notifications);
45
46 // cache size calculations
47- const auto cache_sizes = CalculateCacheSizes(args, g_enabled_filter_types.size());
48- if (!cache_sizes) {
49- return InitError(_("Failed to calculate cache sizes. Try lowering the -dbcache value."));
50- }
51- auto [index_cache_sizes, kernel_cache_sizes] = *cache_sizes;
52+ const auto [index_cache_sizes, kernel_cache_sizes] = CalculateCacheSizes(args, g_enabled_filter_types.size());
53
54 LogInfo("Cache configuration:");
55 LogInfo("* Using %.1f MiB for block index database", kernel_cache_sizes.block_tree_db * (1.0 / 1024 / 1024));
56diff --git a/src/kernel/caches.h b/src/kernel/caches.h
57index 9ee5a31e0a..4414d1a123 100644
58--- a/src/kernel/caches.h
59+++ b/src/kernel/caches.h
60@@ -5,16 +5,16 @@
61 #ifndef BITCOIN_KERNEL_CACHES_H
62 #define BITCOIN_KERNEL_CACHES_H
63
64-#include <util/byte_conversion.h>
65+#include <util/storage.h>
66
67 #include <algorithm>
68
69-//! Suggested default amount of cache reserved for the kernel (MiB)
70-static constexpr int64_t DEFAULT_KERNEL_CACHE{450};
71+//! Suggested default amount of cache reserved for the kernel (bytes)
72+static constexpr size_t DEFAULT_KERNEL_CACHE{450_MiB};
73 //! Max memory allocated to block tree DB specific cache (bytes)
74-static constexpr size_t MAX_BLOCK_DB_CACHE{MiBToBytes(2)};
75+static constexpr size_t MAX_BLOCK_DB_CACHE{2_MiB};
76 //! Max memory allocated to coin DB specific cache (bytes)
77-static constexpr size_t MAX_COINS_DB_CACHE{MiBToBytes(8)};
78+static constexpr size_t MAX_COINS_DB_CACHE{8_MiB};
79
80 namespace kernel {
81 struct CacheSizes {
82diff --git a/src/node/caches.cpp b/src/node/caches.cpp
83index 41a8971665..94ad5bbe61 100644
84--- a/src/node/caches.cpp
85+++ b/src/node/caches.cpp
86@@ -8,34 +8,27 @@
87 #include <index/txindex.h>
88 #include <kernel/caches.h>
89 #include <logging.h>
90-#include <util/byte_conversion.h>
91+#include <util/overflow.h>
92+#include <util/storage.h>
93
94 #include <algorithm>
95-#include <optional>
96 #include <stdexcept>
97 #include <string>
98
99 // Unlike for the UTXO database, for the txindex scenario the leveldb cache make
100 // a meaningful difference: [#8273 (comment)](/bitcoin-bitcoin/8273/#issuecomment-229601991)
101 //! Max memory allocated to tx index DB specific cache in bytes.
102-static constexpr size_t MAX_TX_INDEX_CACHE{MiBToBytes(1024)};
103+static constexpr size_t MAX_TX_INDEX_CACHE{1024_MiB};
104 //! Max memory allocated to all block filter index caches combined in bytes.
105-static constexpr size_t MAX_FILTER_INDEX_CACHE{MiBToBytes(1024)};
106+static constexpr size_t MAX_FILTER_INDEX_CACHE{1024_MiB};
107
108 namespace node {
109-std::optional<CacheSizes> CalculateCacheSizes(const ArgsManager& args, size_t n_indexes)
110+CacheSizes CalculateCacheSizes(const ArgsManager& args, size_t n_indexes)
111 {
112- int64_t db_cache = args.GetIntArg("-dbcache", DEFAULT_DB_CACHE);
113-
114- // negative values are permitted, but interpreted as zero.
115- db_cache = std::max(int64_t{0}, db_cache);
116-
117- size_t total_cache = 0;
118- try {
119- total_cache = std::max(MiBToBytes(db_cache), MiBToBytes(MIN_DB_CACHE));
120- } catch (const std::out_of_range&) {
121- LogError("Cannot allocate more than %d MiB in total for db caches.", std::numeric_limits<size_t>::max() >> 20);
122- return std::nullopt;
123+ // Total cache size in MiB, floored by MIN_DB_CACHE and capped by max size_t value
124+ size_t total_cache{DEFAULT_DB_CACHE};
125+ if (auto db_cache = args.GetIntArg("-dbcache")) {
126+ total_cache = std::max(MIN_DB_CACHE, SaturatingLeftShift<size_t>(*db_cache, 20));
127 }
128
129 IndexCacheSizes index_sizes;
130@@ -47,6 +40,6 @@ std::optional<CacheSizes> CalculateCacheSizes(const ArgsManager& args, size_t n_
131 index_sizes.filter_index = max_cache / n_indexes;
132 total_cache -= index_sizes.filter_index * n_indexes;
133 }
134- return {{index_sizes, kernel::CacheSizes{total_cache}}};
135+ return {index_sizes, kernel::CacheSizes{total_cache}};
136 }
137 } // namespace node
138diff --git a/src/node/caches.h b/src/node/caches.h
139index b4f2e3d516..aab7871e93 100644
140--- a/src/node/caches.h
141+++ b/src/node/caches.h
142@@ -6,17 +6,16 @@
143 #define BITCOIN_NODE_CACHES_H
144
145 #include <kernel/caches.h>
146-#include <util/byte_conversion.h>
147+#include <util/storage.h>
148
149 #include <cstddef>
150-#include <optional>
151
152 class ArgsManager;
153
154-//! min. -dbcache (MiB)
155-static constexpr int64_t MIN_DB_CACHE{4};
156-//! -dbcache default (MiB)
157-static constexpr int64_t DEFAULT_DB_CACHE{DEFAULT_KERNEL_CACHE};
158+//! min. -dbcache (bytes)
159+static constexpr size_t MIN_DB_CACHE{4_MiB};
160+//! -dbcache default (bytes)
161+static constexpr size_t DEFAULT_DB_CACHE{DEFAULT_KERNEL_CACHE};
162
163 namespace node {
164 struct IndexCacheSizes {
165@@ -27,7 +26,7 @@ struct CacheSizes {
166 IndexCacheSizes index;
167 kernel::CacheSizes kernel;
168 };
169-std::optional<CacheSizes> CalculateCacheSizes(const ArgsManager& args, size_t n_indexes = 0);
170+CacheSizes CalculateCacheSizes(const ArgsManager& args, size_t n_indexes = 0);
171 } // namespace node
172
173 #endif // BITCOIN_NODE_CACHES_H
174diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp
175index 76b7852109..2cea1bf6ab 100644
176--- a/src/qt/optionsdialog.cpp
177+++ b/src/qt/optionsdialog.cpp
178@@ -95,7 +95,7 @@ OptionsDialog::OptionsDialog(QWidget* parent, bool enableWallet)
179 ui->verticalLayout->setStretchFactor(ui->tabWidget, 1);
180
181 /* Main elements init */
182- ui->databaseCache->setRange(MIN_DB_CACHE, std::numeric_limits<int>::max());
183+ ui->databaseCache->setRange(MIN_DB_CACHE >> 20, std::numeric_limits<int>::max());
184 ui->threadsScriptVerif->setMinimum(-GetNumCores());
185 ui->threadsScriptVerif->setMaximum(MAX_SCRIPTCHECK_THREADS);
186 ui->pruneWarning->setVisible(false);
187diff --git a/src/qt/optionsmodel.cpp b/src/qt/optionsmodel.cpp
188index 2c6f13dc6d..5ff22b0f4c 100644
189--- a/src/qt/optionsmodel.cpp
190+++ b/src/qt/optionsmodel.cpp
191@@ -470,7 +470,7 @@ QVariant OptionsModel::getOption(OptionID option, const std::string& suffix) con
192 suffix.empty() ? getOption(option, "-prev") :
193 DEFAULT_PRUNE_TARGET_GB;
194 case DatabaseCache:
195- return qlonglong(SettingToInt(setting(), DEFAULT_DB_CACHE));
196+ return qlonglong(SettingToInt(setting(), DEFAULT_DB_CACHE >> 20));
197 case ThreadsScriptVerif:
198 return qlonglong(SettingToInt(setting(), DEFAULT_SCRIPTCHECK_THREADS));
199 case Listen:
200@@ -733,7 +733,7 @@ void OptionsModel::checkAndMigrate()
201 // see [#8273](/bitcoin-bitcoin/8273/)
202 // force people to upgrade to the new value if they are using 100MB
203 if (settingsVersion < 130000 && settings.contains("nDatabaseCache") && settings.value("nDatabaseCache").toLongLong() == 100)
204- settings.setValue("nDatabaseCache", (qint64)DEFAULT_DB_CACHE);
205+ settings.setValue("nDatabaseCache", (qint64)DEFAULT_DB_CACHE >> 20);
206
207 settings.setValue(strSettingsVersionKey, CLIENT_VERSION);
208 }
209diff --git a/src/test/util/setup_common.h b/src/test/util/setup_common.h
210index 7d7c7b7cd4..33ad258457 100644
211--- a/src/test/util/setup_common.h
212+++ b/src/test/util/setup_common.h
213@@ -104,7 +104,7 @@ struct BasicTestingSetup {
214 * initialization behaviour.
215 */
216 struct ChainTestingSetup : public BasicTestingSetup {
217- kernel::CacheSizes m_kernel_cache_sizes{node::CalculateCacheSizes(m_args).value().kernel};
218+ kernel::CacheSizes m_kernel_cache_sizes{node::CalculateCacheSizes(m_args).kernel};
219 bool m_coins_db_in_memory{true};
220 bool m_block_tree_db_in_memory{true};
221 std::function<void()> m_make_chainman{};
222diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp
223index e8e0abbda6..dbe1d76ca8 100644
224--- a/src/test/util_tests.cpp
225+++ b/src/test/util_tests.cpp
226@@ -13,12 +13,12 @@
227 #include <test/util/setup_common.h>
228 #include <uint256.h>
229 #include <util/bitdeque.h>
230-#include <util/byte_conversion.h>
231 #include <util/fs.h>
232 #include <util/fs_helpers.h>
233 #include <util/moneystr.h>
234 #include <util/overflow.h>
235 #include <util/readwritefile.h>
236+#include <util/storage.h>
237 #include <util/strencodings.h>
238 #include <util/string.h>
239 #include <util/time.h>
240@@ -141,19 +141,6 @@ BOOST_AUTO_TEST_CASE(util_criticalsection)
241 } while(0);
242 }
243
244-BOOST_AUTO_TEST_CASE(byte_conversion)
245-{
246- // maximum allowed value in MiB
247- const int64_t max_cache = std::numeric_limits<size_t>::max() >> 20;
248-
249- BOOST_CHECK_EXCEPTION(MiBToBytes(-1), std::out_of_range, HasReason("Value may not be negative."));
250- BOOST_CHECK_EXCEPTION(MiBToBytes(std::numeric_limits<int64_t>::max()), std::out_of_range, HasReason("Conversion to bytes of"));
251- BOOST_CHECK_EXCEPTION(MiBToBytes(max_cache + 1), std::out_of_range, HasReason("Conversion to bytes of"));
252-
253- BOOST_CHECK_EQUAL(MiBToBytes(0), 0);
254- BOOST_CHECK_EQUAL(MiBToBytes(max_cache), max_cache << 20);
255-}
256-
257 constexpr char HEX_PARSE_INPUT[] = "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f";
258 constexpr uint8_t HEX_PARSE_OUTPUT[] = {
259 0x04, 0x67, 0x8a, 0xfd, 0xb0, 0xfe, 0x55, 0x48, 0x27, 0x19, 0x67, 0xf1, 0xa6, 0x71, 0x30, 0xb7,
260@@ -1892,4 +1879,63 @@ BOOST_AUTO_TEST_CASE(clearshrink_test)
261 }
262 }
263
264+template <typename T>
265+void TestCheckedLeftShift()
266+{
267+ BOOST_TEST_CONTEXT("Testing CheckedLeftShift with " << typeid(T).name()) {
268+ // Basic operations
269+ BOOST_CHECK_EQUAL(CheckedLeftShift<T>(0, 1), 0);
270+ BOOST_CHECK_EQUAL(CheckedLeftShift<T>(1, 1), 2);
271+ BOOST_CHECK_EQUAL(CheckedLeftShift<T>(2, 2), 8);
272+
273+ // Negative input
274+ BOOST_CHECK(!CheckedLeftShift<T>(-1, 1));
275+
276+ // Overflow cases
277+ BOOST_CHECK(!CheckedLeftShift<T>((std::numeric_limits<T>::max() >> 1) + 1, 1));
278+ BOOST_CHECK(!CheckedLeftShift<T>(std::numeric_limits<T>::max(), 1));
279+ }
280+}
281+
282+template <typename T>
283+void TestSaturatingLeftShift()
284+{
285+ BOOST_TEST_CONTEXT("Testing SaturatingLeftShift with " << typeid(T).name()) {
286+ // Basic operations
287+ BOOST_CHECK_EQUAL(SaturatingLeftShift<T>(0, 1), 0);
288+ BOOST_CHECK_EQUAL(SaturatingLeftShift<T>(1, 1), 2);
289+ BOOST_CHECK_EQUAL(SaturatingLeftShift<T>(2, 2), 8);
290+
291+ // Negative input
292+ BOOST_CHECK_EQUAL(SaturatingLeftShift<T>(-1, 1), 0);
293+
294+ // Saturation cases
295+ BOOST_CHECK_EQUAL(SaturatingLeftShift<T>((std::numeric_limits<T>::max() >> 1) + 1, 1), std::numeric_limits<T>::max());
296+ BOOST_CHECK_EQUAL(SaturatingLeftShift<T>(std::numeric_limits<T>::max(), 1), std::numeric_limits<T>::max());
297+ }
298+}
299+
300+BOOST_AUTO_TEST_CASE(checked_left_shift_test)
301+{
302+ TestCheckedLeftShift<uint8_t>();
303+ TestCheckedLeftShift<size_t>();
304+ TestCheckedLeftShift<uint64_t>();
305+}
306+
307+BOOST_AUTO_TEST_CASE(saturating_left_shift_test)
308+{
309+ TestSaturatingLeftShift<uint8_t>();
310+ TestSaturatingLeftShift<size_t>();
311+ TestSaturatingLeftShift<uint64_t>();
312+}
313+
314+BOOST_AUTO_TEST_CASE(mib_string_literal_test)
315+{
316+ BOOST_CHECK_EQUAL(0_MiB, 0);
317+ BOOST_CHECK_EQUAL(1_MiB, 1024 * 1024);
318+
319+ constexpr size_t max_safe_mib = std::numeric_limits<size_t>::max() / (1024 * 1024);
320+ BOOST_CHECK_EQUAL(max_safe_mib * (1024 * 1024), max_safe_mib * 1_MiB);
321+}
322+
323 BOOST_AUTO_TEST_SUITE_END()
324diff --git a/src/util/byte_conversion.h b/src/util/byte_conversion.h
325deleted file mode 100644
326index bbdad04bff..0000000000
327--- a/src/util/byte_conversion.h
328+++ /dev/null
329@@ -1,26 +0,0 @@
330-// Copyright (c) 2025-present The Bitcoin Core developers
331-// Distributed under the MIT software license, see the accompanying
332-// file COPYING or http://www.opensource.org/licenses/mit-license.php.
333-
334-#ifndef BITCOIN_UTIL_BYTE_CONVERSION_H
335-#define BITCOIN_UTIL_BYTE_CONVERSION_H
336-
337-#include <tinyformat.h>
338-
339-#include <cstdint>
340-#include <limits>
341-#include <stdexcept>
342-
343-//! Guard against truncation of values before converting.
344-constexpr size_t MiBToBytes(int64_t mib)
345-{
346- if (mib < 0) {
347- throw std::out_of_range("Value may not be negative.");
348- }
349- if (static_cast<uint64_t>(mib) > std::numeric_limits<size_t>::max() >> 20) {
350- 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()));
351- }
352- return static_cast<size_t>(mib) << 20;
353-}
354-
355-#endif // BITCOIN_UTIL_BYTE_CONVERSION_H
356diff --git a/src/util/overflow.h b/src/util/overflow.h
357index 7e0cce6c27..bb5277db28 100644
358--- a/src/util/overflow.h
359+++ b/src/util/overflow.h
360@@ -47,4 +47,42 @@ template <class T>
361 return i + j;
362 }
363
364+/**
365+ * [@brief](/bitcoin-bitcoin/contributor/brief/) Left bit shift with overflow checking.
366+ * [@param](/bitcoin-bitcoin/contributor/param/) i The input value to be left shifted.
367+ * [@param](/bitcoin-bitcoin/contributor/param/) shift The number of bits to left shift.
368+ * [@return](/bitcoin-bitcoin/contributor/return/) The result of the left shift, or std::nullopt in case of
369+ * overflow or negative input value.
370+ */
371+template <class Output, class Input>
372+constexpr std::optional<Output> CheckedLeftShift(Input i, unsigned shift) noexcept
373+ requires std::is_unsigned_v<Output>
374+{
375+ if constexpr (std::is_signed_v<Input>) {
376+ if (i < 0) return std::nullopt;
377+ }
378+ if (std::make_unsigned_t<Input>(i) > (std::numeric_limits<Output>::max() >> shift)) {
379+ return std::nullopt;
380+ }
381+ return i << shift;
382+}
383+
384+/**
385+ * [@brief](/bitcoin-bitcoin/contributor/brief/) Left bit shift with safe minimum and maximum values.
386+ * [@param](/bitcoin-bitcoin/contributor/param/) i The input value to be left shifted.
387+ * [@param](/bitcoin-bitcoin/contributor/param/) shift The number of bits to left shift.
388+ * [@return](/bitcoin-bitcoin/contributor/return/) The result of the left shift, with the return value clamped
389+ * between zero and the maximum Output value if overflow occurs.
390+ */
391+template <class Output, class Input>
392+constexpr Output SaturatingLeftShift(Input i, unsigned shift) noexcept
393+ requires std::is_unsigned_v<Output>
394+{
395+ auto default_value{std::numeric_limits<Output>::max()};
396+ if constexpr (std::is_signed_v<Input>) {
397+ if (i < 0) default_value = 0;
398+ }
399+ return CheckedLeftShift<Output>(i, shift).value_or(default_value);
400+}
401+
402 #endif // BITCOIN_UTIL_OVERFLOW_H
403diff --git a/src/util/storage.h b/src/util/storage.h
404new file mode 100644
405index 0000000000..042875510b
406--- /dev/null
407+++ b/src/util/storage.h
408@@ -0,0 +1,24 @@
409+// Copyright (c) 2025-present The Bitcoin Core developers
410+// Distributed under the MIT software license, see the accompanying
411+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
412+
413+#ifndef BITCOIN_UTIL_STORAGE_H
414+#define BITCOIN_UTIL_STORAGE_H
415+
416+#include <util/overflow.h>
417+
418+#include <cstdint>
419+#include <limits>
420+#include <stdexcept>
421+
422+//! Overflow-safe conversion of MiB to bytes.
423+constexpr size_t operator"" _MiB(unsigned long long mebibytes)
424+{
425+ auto bytes{CheckedLeftShift<size_t>(static_cast<size_t>(mebibytes), 20)};
426+ if (!bytes) {
427+ throw std::overflow_error("mebibytes cannot be converted to bytes");
428+ }
429+ return *bytes;
430+}
431+
432+#endif // BITCOIN_UTIL_STORAGE_H