…I guess that could work too.
  0diff --git a/src/test/util_string_tests.cpp b/src/test/util_string_tests.cpp
  1index c6ee1ffed9..2f6fad50b7 100644
  2--- a/src/test/util_string_tests.cpp
  3+++ b/src/test/util_string_tests.cpp
  4@@ -7,74 +7,172 @@
  5 #include <boost/test/unit_test.hpp>
  6 
  7 using namespace util;
  8+using util::detail::CountFormatSpecifiers;
  9+
 10+namespace {
 11+bool CheckTooMany(const tfm::format_error& s) { return s.what() == std::string_view{"tinyformat: Too many conversion specifiers in format string"}; }
 12+bool CheckOutOfRange(const tfm::format_error& s) { return s.what() == std::string_view{"tinyformat: Positional argument out of range"}; }
 13+}
 14 
 15 BOOST_AUTO_TEST_SUITE(util_string_tests)
 16 
 17-BOOST_AUTO_TEST_CASE(ConstevalFormatString_NumSpec)
 18+// These are counted correctly, since tinyformat will require the provided number of args.
 19+BOOST_AUTO_TEST_CASE(ConstevalFormatString_CorrectCounts)
 20 {
 21-    // Compile-time sanity checks
 22-    static_assert([] {
 23-        ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("");
 24-        ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("%%");
 25-        ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%s");
 26-        ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("%%s");
 27-        ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("s%%");
 28-        ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%%%s");
 29-        ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%s%%");
 30-        ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers(" 1$s");
 31-        ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%1$s");
 32-        ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%1$s%1$s");
 33-        ConstevalFormatString<2>::Detail_CheckNumFormatSpecifiers("%2$s");
 34-        ConstevalFormatString<2>::Detail_CheckNumFormatSpecifiers("%2$s 4$s %2$s");
 35-        ConstevalFormatString<129>::Detail_CheckNumFormatSpecifiers("%129$s 999$s %2$s");
 36-        ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%02d");
 37-        ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%+2s");
 38-        ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%+2s");
 39-        ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%.6i");
 40-        ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%5.2f");
 41-        ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%#x");
 42-        ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%1$5i");
 43-        ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%1$-5i");
 44-        ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%1$.5i");
 45-        // tinyformat accepts almost any "type" spec, even '%', or '_', or '\n'.
 46-        ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%123%");
 47-        ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%123%s");
 48-        ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%_");
 49-        ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%\n");
 50-        return true; // All checks above compiled and passed
 51-    }());
 52-    static_assert([] {
 53+    static_assert(CountFormatSpecifiers("") == 0);
 54+    tfm::format("");
 55+
 56+    static_assert(CountFormatSpecifiers("%%") == 0);
 57+    tfm::format("%%");
 58+
 59+    static_assert(CountFormatSpecifiers("%s") == 1);
 60+    BOOST_CHECK_EXCEPTION(tfm::format("%s"), tfm::format_error, CheckTooMany);
 61+    tfm::format("%s", "foo");
 62+
 63+    static_assert(CountFormatSpecifiers("%%s") == 0);
 64+    tfm::format("%%s");
 65+
 66+    static_assert(CountFormatSpecifiers("s%%") == 0);
 67+    tfm::format("s%%");
 68+
 69+    static_assert(CountFormatSpecifiers("%%%s") == 1);
 70+    BOOST_CHECK_EXCEPTION(tfm::format("%%%s"), tfm::format_error, CheckTooMany);
 71+    tfm::format("%%%s", "foo");
 72+
 73+    static_assert(CountFormatSpecifiers("%s%%") == 1);
 74+    BOOST_CHECK_EXCEPTION(tfm::format("%s%%"), tfm::format_error, CheckTooMany);
 75+    tfm::format("%s%%", "foo");
 76+
 77+    static_assert(CountFormatSpecifiers(" 1$s") == 0);
 78+    tfm::format(" 1$s");
 79+
 80+    static_assert(CountFormatSpecifiers("%1$s") == 1);
 81+    BOOST_CHECK_EXCEPTION(tfm::format("%1$s"), tfm::format_error, CheckOutOfRange);
 82+    tfm::format("%1$s", "foo");
 83+
 84+    static_assert(CountFormatSpecifiers("%1$s%1$s") == 1);
 85+    BOOST_CHECK_EXCEPTION(tfm::format("%1$s%1$s"), tfm::format_error, CheckOutOfRange);
 86+    tfm::format("%1$s%1$s", "foo");
 87+
 88+    static_assert(CountFormatSpecifiers("%2$s") == 2);
 89+    BOOST_CHECK_EXCEPTION(tfm::format("%2$s"), tfm::format_error, CheckOutOfRange);
 90+    BOOST_CHECK_EXCEPTION(tfm::format("%2$s", "foo"), tfm::format_error, CheckOutOfRange);
 91+    tfm::format("%2$s", "foo", "bar");
 92+
 93+    static_assert(CountFormatSpecifiers("%2$s 4$s %2$s") == 2);
 94+    BOOST_CHECK_EXCEPTION(tfm::format("%2$s 4$s %2$s"), tfm::format_error, CheckOutOfRange);
 95+    BOOST_CHECK_EXCEPTION(tfm::format("%2$s 4$s %2$s", "foo"), tfm::format_error, CheckOutOfRange);
 96+    tfm::format("%2$s 4$s %2$s", "foo", "bar");
 97+
 98+    static_assert(CountFormatSpecifiers("%12$s 99$s %2$s") == 12);
 99+    BOOST_CHECK_EXCEPTION(tfm::format("%12$s 99$s %2$s"), tfm::format_error, CheckOutOfRange);
100+    BOOST_CHECK_EXCEPTION(tfm::format("%12$s 99$s %2$s", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"), tfm::format_error, CheckOutOfRange);
101+    tfm::format("%12$s 99$s %2$s", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12");
102+
103+    static_assert(CountFormatSpecifiers("%02d") == 1);
104+    BOOST_CHECK_EXCEPTION(tfm::format("%02d"), tfm::format_error, CheckTooMany);
105+    tfm::format("%02d", 1);
106+
107+    static_assert(CountFormatSpecifiers("%+2s") == 1);
108+    BOOST_CHECK_EXCEPTION(tfm::format("%+2s"), tfm::format_error, CheckTooMany);
109+    tfm::format("%+2s", 1);
110+
111+    static_assert(CountFormatSpecifiers("%.6i") == 1);
112+    BOOST_CHECK_EXCEPTION(tfm::format("%.6i"), tfm::format_error, CheckTooMany);
113+    tfm::format("%.6i", 1);
114+
115+    static_assert(CountFormatSpecifiers("%5.2f") == 1);
116+    BOOST_CHECK_EXCEPTION(tfm::format("%5.2f"), tfm::format_error, CheckTooMany);
117+    tfm::format("%5.2f", 1);
118+
119+    static_assert(CountFormatSpecifiers("%1$5i") == 1);
120+    BOOST_CHECK_EXCEPTION(tfm::format("%1$5i"), tfm::format_error, CheckOutOfRange);
121+    tfm::format("%1$5i", 1);
122+
123+    static_assert(CountFormatSpecifiers("%1$-5i") == 1);
124+    BOOST_CHECK_EXCEPTION(tfm::format("%1$-5i"), tfm::format_error, CheckOutOfRange);
125+    tfm::format("%1$-5i", 1);
126+
127+    static_assert(CountFormatSpecifiers("%1$.5i") == 1);
128+    BOOST_CHECK_EXCEPTION(tfm::format("%1$.5i"), tfm::format_error, CheckOutOfRange);
129+    tfm::format("%1$.5i", 1);
130+
131+    static_assert(CountFormatSpecifiers("%#x") == 1);
132+    BOOST_CHECK_EXCEPTION(tfm::format("%#x"), tfm::format_error, CheckTooMany);
133+    tfm::format("%#x", 1);
134+
135+    static_assert(CountFormatSpecifiers("%123%") == 1);
136+    BOOST_CHECK_EXCEPTION(tfm::format("%123%"), tfm::format_error, CheckTooMany);
137+    tfm::format("%123%", 1);
138+
139+    static_assert(CountFormatSpecifiers("%123%s") == 1);
140+    BOOST_CHECK_EXCEPTION(tfm::format("%123%s"), tfm::format_error, CheckTooMany);
141+    tfm::format("%123%s", 1);
142+
143+    static_assert(CountFormatSpecifiers("%_") == 1);
144+    BOOST_CHECK_EXCEPTION(tfm::format("%_"), tfm::format_error, CheckTooMany);
145+    tfm::format("%_", 1);
146+
147+    static_assert(CountFormatSpecifiers("%\n") == 1);
148+    BOOST_CHECK_EXCEPTION(tfm::format("%\n"), tfm::format_error, CheckTooMany);
149+    tfm::format("%\n", 1);
150+}
151+
152 // The `*` specifier behavior is unsupported and can lead to runtime
153 // errors when used in a ConstevalFormatString. Please refer to the
154 // note in the ConstevalFormatString docs.
155-        ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%*c");
156-        ConstevalFormatString<2>::Detail_CheckNumFormatSpecifiers("%2$*3$d");
157-        ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%.*f");
158-        return true; // All checks above compiled and passed
159-    }());
160+BOOST_AUTO_TEST_CASE(ConstevalFormatString_IncorrectCounts)
161+{
162+    int a{}, b{}, c{};
163 
164-    // Negative checks at runtime
165+    auto check_not_enough{[](const tfm::format_error& s) { return s.what() == std::string_view{"tinyformat: Not enough arguments to read variable width or precision"}; }};
166+    static_assert(CountFormatSpecifiers("%*c") == 1);
167+    BOOST_CHECK_EXCEPTION(tfm::format("%*c"), tfm::format_error, check_not_enough);
168+    BOOST_CHECK_EXCEPTION(tfm::format("%*c", a), tfm::format_error, CheckTooMany);
169+    tfm::format("%*c", a, b);
170+
171+    static_assert(CountFormatSpecifiers("%2$*3$d") == 2);
172+    BOOST_CHECK_EXCEPTION(tfm::format("%2$*3$d"), tfm::format_error, CheckOutOfRange);
173+    BOOST_CHECK_EXCEPTION(tfm::format("%2$*3$d", a), tfm::format_error, CheckOutOfRange);
174+    BOOST_CHECK_EXCEPTION(tfm::format("%2$*3$d", a, b), tfm::format_error, CheckOutOfRange);
175+    tfm::format("%2$*3$d", a, b, c);
176+
177+    static_assert(CountFormatSpecifiers("%.*f") == 1);
178+    BOOST_CHECK_EXCEPTION(tfm::format("%.*f"), tfm::format_error, check_not_enough);
179+    BOOST_CHECK_EXCEPTION(tfm::format("%.*f", a), tfm::format_error, CheckTooMany);
180+    tfm::format("%.*f", a, b);
181+}
182+
183+// Negative checks. Executed at runtime as exceptions are not allowed at compile time.
184+BOOST_AUTO_TEST_CASE(ConstevalFormatString_NegativeChecks)
185+{
186     using ErrType = const char*;
187 
188     auto check_mix{[](const ErrType& str) { return str == std::string_view{"Format specifiers must be all positional or all non-positional!"}; }};
189-    BOOST_CHECK_EXCEPTION(ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%s%1$s"), ErrType, check_mix);
190-
191-    auto check_num_spec{[](const ErrType& str) { return str == std::string_view{"Format specifier count must match the argument count!"}; }};
192-    BOOST_CHECK_EXCEPTION(ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers(""), ErrType, check_num_spec);
193-    BOOST_CHECK_EXCEPTION(ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("%s"), ErrType, check_num_spec);
194-    BOOST_CHECK_EXCEPTION(ConstevalFormatString<2>::Detail_CheckNumFormatSpecifiers("%s"), ErrType, check_num_spec);
195-    BOOST_CHECK_EXCEPTION(ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("%1$s"), ErrType, check_num_spec);
196-    BOOST_CHECK_EXCEPTION(ConstevalFormatString<2>::Detail_CheckNumFormatSpecifiers("%1$s"), ErrType, check_num_spec);
197+    BOOST_CHECK_EXCEPTION(CountFormatSpecifiers("%s%1$s"), ErrType, check_mix);
198 
199     auto check_0_pos{[](const ErrType& str) { return str == std::string_view{"Positional format specifier must have position of at least 1"}; }};
200-    BOOST_CHECK_EXCEPTION(ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%$s"), ErrType, check_0_pos);
201-    BOOST_CHECK_EXCEPTION(ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%$"), ErrType, check_0_pos);
202-    BOOST_CHECK_EXCEPTION(ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("%0$"), ErrType, check_0_pos);
203-    BOOST_CHECK_EXCEPTION(ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("%0$s"), ErrType, check_0_pos);
204+    BOOST_CHECK_EXCEPTION(CountFormatSpecifiers("%$s"), ErrType, check_0_pos);
205+    BOOST_CHECK_EXCEPTION(CountFormatSpecifiers("%$"), ErrType, check_0_pos);
206+    BOOST_CHECK_EXCEPTION(CountFormatSpecifiers("%0$"), ErrType, check_0_pos);
207+    BOOST_CHECK_EXCEPTION(CountFormatSpecifiers("%0$s"), ErrType, check_0_pos);
208 
209     auto check_term{[](const ErrType& str) { return str == std::string_view{"Format specifier incorrectly terminated by end of string"}; }};
210-    BOOST_CHECK_EXCEPTION(ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%"), ErrType, check_term);
211-    BOOST_CHECK_EXCEPTION(ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%1$"), ErrType, check_term);
212+    BOOST_CHECK_EXCEPTION(CountFormatSpecifiers("%"), ErrType, check_term);
213+    BOOST_CHECK_EXCEPTION(CountFormatSpecifiers("%1$"), ErrType, check_term);
214+}
215+
216+// Existing "tests" in run-lint-format-strings.py parse_string_content
217+BOOST_AUTO_TEST_CASE(ConstevalFormatString_run_lint_format_strings_parse_string_content)
218+{
219+    static_assert(CountFormatSpecifiers("foo bar foo") == 0);
220+    static_assert(CountFormatSpecifiers("foo %d bar foo") == 1);
221+    static_assert(CountFormatSpecifiers("foo %d bar %i foo") == 2);
222+    static_assert(CountFormatSpecifiers("foo %d bar %i foo %% foo") == 2);
223+    static_assert(CountFormatSpecifiers("foo %d bar %i foo %% foo %d foo") == 3);
224+    //static_assert(CountFormatSpecifiers("foo %d bar %i foo %% foo %*d foo") == 4); // not implemented
225+    static_assert(CountFormatSpecifiers("foo %5$d") == 5);
226+    //static_assert(CountFormatSpecifiers("foo %5$*7$d") == 7); // not implemented
227 }
228 
229 BOOST_AUTO_TEST_SUITE_END()
230diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp
231index 1624fb8b5b..bd40d596ef 100644
232--- a/src/test/util_tests.cpp
233+++ b/src/test/util_tests.cpp
234@@ -150,22 +150,24 @@ constexpr uint8_t HEX_PARSE_OUTPUT[] = {
235 static_assert((sizeof(HEX_PARSE_INPUT) - 1) == 2 * sizeof(HEX_PARSE_OUTPUT));
236 BOOST_AUTO_TEST_CASE(parse_hex)
237 {
238+    using util::hex_literals::detail::Hex;
239+
240     std::vector<unsigned char> result;
241 
242     // Basic test vector
243     std::vector<unsigned char> expected(std::begin(HEX_PARSE_OUTPUT), std::end(HEX_PARSE_OUTPUT));
244-    constexpr std::array<std::byte, 65> hex_literal_array{operator""_hex<util::detail::Hex(HEX_PARSE_INPUT)>()};
245+    constexpr std::array<std::byte, 65> hex_literal_array{operator""_hex<Hex(HEX_PARSE_INPUT)>()};
246     auto hex_literal_span{MakeUCharSpan(hex_literal_array)};
247     BOOST_CHECK_EQUAL_COLLECTIONS(hex_literal_span.begin(), hex_literal_span.end(), expected.begin(), expected.end());
248 
249-    const std::vector<std::byte> hex_literal_vector{operator""_hex_v<util::detail::Hex(HEX_PARSE_INPUT)>()};
250+    const std::vector<std::byte> hex_literal_vector{operator""_hex_v<Hex(HEX_PARSE_INPUT)>()};
251     hex_literal_span = MakeUCharSpan(hex_literal_vector);
252     BOOST_CHECK_EQUAL_COLLECTIONS(hex_literal_span.begin(), hex_literal_span.end(), expected.begin(), expected.end());
253 
254-    constexpr std::array<uint8_t, 65> hex_literal_array_uint8{operator""_hex_u8<util::detail::Hex(HEX_PARSE_INPUT)>()};
255+    constexpr std::array<uint8_t, 65> hex_literal_array_uint8{operator""_hex_u8<Hex(HEX_PARSE_INPUT)>()};
256     BOOST_CHECK_EQUAL_COLLECTIONS(hex_literal_array_uint8.begin(), hex_literal_array_uint8.end(), expected.begin(), expected.end());
257 
258-    result = operator""_hex_v_u8<util::detail::Hex(HEX_PARSE_INPUT)>();
259+    result = operator""_hex_v_u8<Hex(HEX_PARSE_INPUT)>();
260     BOOST_CHECK_EQUAL_COLLECTIONS(result.begin(), result.end(), expected.begin(), expected.end());
261 
262     result = ParseHex(HEX_PARSE_INPUT);
263diff --git a/src/util/strencodings.h b/src/util/strencodings.h
264index 1543de03ab..b79806c251 100644
265--- a/src/util/strencodings.h
266+++ b/src/util/strencodings.h
267@@ -427,16 +427,16 @@ struct Hex {
268 
269 } // namespace detail
270 
271-template <util::detail::Hex str>
272+template <util::hex_literals::detail::Hex str>
273 constexpr auto operator""_hex() { return str.bytes; }
274 
275-template <util::detail::Hex str>
276+template <util::hex_literals::detail::Hex str>
277 constexpr auto operator""_hex_u8() { return std::bit_cast<std::array<uint8_t, str.bytes.size()>>(str.bytes); }
278 
279-template <util::detail::Hex str>
280+template <util::hex_literals::detail::Hex str>
281 constexpr auto operator""_hex_v() { return std::vector<std::byte>{str.bytes.begin(), str.bytes.end()}; }
282 
283-template <util::detail::Hex str>
284+template <util::hex_literals::detail::Hex str>
285 inline auto operator""_hex_v_u8() { return std::vector<uint8_t>{UCharCast(str.bytes.data()), UCharCast(str.bytes.data() + str.bytes.size())}; }
286 
287 } // inline namespace hex_literals
288diff --git a/src/util/string.h b/src/util/string.h
289index 4724d881ff..d99c1963c6 100644
290--- a/src/util/string.h
291+++ b/src/util/string.h
292@@ -18,25 +18,9 @@
293 #include <vector>
294 
295 namespace util {
296-/**
297- * [@brief](/bitcoin-bitcoin/contributor/brief/) A wrapper for a compile-time partially validated format string
298- *
299- * This struct can be used to enforce partial compile-time validation of format
300- * strings, to reduce the likelihood of tinyformat throwing exceptions at
301- * run-time. Validation is partial to try and prevent the most common errors
302- * while avoiding re-implementing the entire parsing logic.
303- *
304- * [@note](/bitcoin-bitcoin/contributor/note/) Counting of `*` dynamic width and precision fields (such as `%*c`,
305- * `%2$*3$d`, `%.*f`) is not implemented to minimize code complexity as long as
306- * they are not used in the codebase. Usage of these fields is not counted and
307- * can lead to run-time exceptions. Code wanting to use the `*` specifier can
308- * side-step this struct and call tinyformat directly.
309- */
310-template <unsigned num_params>
311-struct ConstevalFormatString {
312-    const char* const fmt;
313-    consteval ConstevalFormatString(const char* str) : fmt{str} { Detail_CheckNumFormatSpecifiers(fmt); }
314-    constexpr static void Detail_CheckNumFormatSpecifiers(std::string_view str)
315+
316+namespace detail {
317+constexpr static unsigned CountFormatSpecifiers(std::string_view str)
318 {
319     unsigned count_normal{0}; // Number of "normal" specifiers, like %s
320     unsigned count_pos{0};    // Max number in positional specifier, like %8$s
321@@ -74,8 +58,31 @@ struct ConstevalFormatString {
322         // specifier is not checked. Parsing continues with the next '%'.
323     }
324     if (count_normal && count_pos) throw "Format specifiers must be all positional or all non-positional!";
325-        unsigned count{count_normal | count_pos};
326-        if (num_params != count) throw "Format specifier count must match the argument count!";
327+    return count_normal | count_pos;
328+}
329+} // namespace detail
330+
331+/**
332+ * [@brief](/bitcoin-bitcoin/contributor/brief/) A wrapper for a compile-time partially validated format string
333+ *
334+ * This struct can be used to enforce partial compile-time validation of format
335+ * strings, to reduce the likelihood of tinyformat throwing exceptions at
336+ * run-time. Validation is partial to try and prevent the most common errors
337+ * while avoiding re-implementing the entire parsing logic.
338+ *
339+ * [@note](/bitcoin-bitcoin/contributor/note/) Counting of `*` dynamic width and precision fields (such as `%*c`,
340+ * `%2$*3$d`, `%.*f`) is not implemented to minimize code complexity as long as
341+ * they are not used in the codebase. Usage of these fields is not counted and
342+ * can lead to run-time exceptions. Code wanting to use the `*` specifier can
343+ * side-step this struct and call tinyformat directly.
344+ */
345+template <unsigned num_params>
346+struct ConstevalFormatString {
347+    const char* const fmt;
348+    consteval ConstevalFormatString(const char* str) : fmt{str}
349+    {
350+        if (detail::CountFormatSpecifiers(fmt) != num_params)
351+            throw "Format specifier count must match the argument count!";
352     }
353 };