From 810cdf6b4e12a1fdace7998d75b4daf8b67d7028 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Sun, 10 Mar 2024 19:49:42 -0400 Subject: [PATCH] tests: overhaul deterministic test randomness The existing code provides two randomness mechanisms for test purposes: - g_insecure_rand_ctx (with its wrappers InsecureRand*), which during tests is initialized using either zeros (SeedRand::ZEROS), or using environment-provided randomness (SeedRand::SEED). - g_mock_deterministic_tests, which controls some (but not all) of the normal randomness output if set, but then makes it extremely predictable (identical output repeatedly). Replace this with a single mechanism, which retains the SeedRand modes to control all randomness. There is a new internal deterministic PRNG inside the random module, which is used in GetRandBytes() when in test mode, and which is also used to initialize g_insecure_rand_ctx. This means that during tests, all random numbers are made deterministic. There is one exception, GetStrongRandBytes(), which even in test mode still uses the normal PRNG state. This probably opens the door to removing a lot of the ad-hoc "deterministic" mode functions littered through the codebase (by simply running relevant tests in SeedRand::ZEROS mode), but this isn't done yet. --- src/random.cpp | 65 +++++++++++++++++++++++++++------- src/random.h | 7 ++++ src/test/bloom_tests.cpp | 8 ++--- src/test/coins_tests.cpp | 5 +-- src/test/cuckoocache_tests.cpp | 10 +++--- src/test/fuzz/fuzz.cpp | 1 + src/test/prevector_tests.cpp | 2 +- src/test/random_tests.cpp | 54 ++++++++++++++++++++-------- src/test/streams_tests.cpp | 2 +- src/test/util/random.cpp | 31 +++++++++------- src/test/util/random.h | 20 ++--------- src/test/util/setup_common.cpp | 2 +- src/test/util_tests.cpp | 2 +- 13 files changed, 133 insertions(+), 76 deletions(-) diff --git a/src/random.cpp b/src/random.cpp index 10ad4e2558a..95e4806aa43 100644 --- a/src/random.cpp +++ b/src/random.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #ifdef WIN32 @@ -417,6 +418,10 @@ class RNGState { uint64_t m_counter GUARDED_BY(m_mutex) = 0; bool m_strongly_seeded GUARDED_BY(m_mutex) = false; + /** If not nullopt, the output of this RNGState is redirected and drawn from here + * (unless always_use_real_rng is passed to MixExtract). */ + std::optional m_deterministic_prng GUARDED_BY(m_mutex); + Mutex m_events_mutex; CSHA256 m_events_hasher GUARDED_BY(m_events_mutex); @@ -457,11 +462,21 @@ public: m_events_hasher.Write(events_hash, 32); } + /** Make the output of MixExtract (unless always_use_real_rng) deterministic, with specified seed. */ + void MakeDeterministic(const uint256& seed) noexcept EXCLUSIVE_LOCKS_REQUIRED(!m_mutex) + { + LOCK(m_mutex); + m_deterministic_prng.emplace(MakeByteSpan(seed)); + } + /** Extract up to 32 bytes of entropy from the RNG state, mixing in new entropy from hasher. * * If this function has never been called with strong_seed = true, false is returned. + * + * If always_use_real_rng is false, and MakeDeterministic has been called before, output + * from the deterministic PRNG instead. */ - bool MixExtract(unsigned char* out, size_t num, CSHA512&& hasher, bool strong_seed) noexcept EXCLUSIVE_LOCKS_REQUIRED(!m_mutex) + bool MixExtract(unsigned char* out, size_t num, CSHA512&& hasher, bool strong_seed, bool always_use_real_rng) noexcept EXCLUSIVE_LOCKS_REQUIRED(!m_mutex) { assert(num <= 32); unsigned char buf[64]; @@ -479,6 +494,13 @@ public: hasher.Finalize(buf); // Store the last 32 bytes of the hash output as new RNG state. memcpy(m_state, buf + 32, 32); + // Handle requests for deterministic randomness. + if (!always_use_real_rng && m_deterministic_prng.has_value()) [[unlikely]] { + // Overwrite the beginning of buf, which will be used for output. + m_deterministic_prng->Keystream(AsWritableBytes(Span{buf, num})); + // Do not require strong seeding for deterministic output. + ret = true; + } } // If desired, copy (up to) the first 32 bytes of the hash output as output. if (num) { @@ -552,8 +574,9 @@ static void SeedSlow(CSHA512& hasher, RNGState& rng) noexcept static void SeedStrengthen(CSHA512& hasher, RNGState& rng, SteadyClock::duration dur) noexcept { // Generate 32 bytes of entropy from the RNG, and a copy of the entropy already in hasher. + // Never use the deterministic PRNG for this, as the result is only used internally. unsigned char strengthen_seed[32]; - rng.MixExtract(strengthen_seed, sizeof(strengthen_seed), CSHA512(hasher), false); + rng.MixExtract(strengthen_seed, sizeof(strengthen_seed), CSHA512(hasher), false, /*always_use_real_rng=*/true); // Strengthen the seed, and feed it into hasher. Strengthen(strengthen_seed, dur, hasher); } @@ -604,7 +627,7 @@ enum class RNGLevel { PERIODIC, //!< Called by RandAddPeriodic() }; -static void ProcRand(unsigned char* out, int num, RNGLevel level) noexcept +static void ProcRand(unsigned char* out, int num, RNGLevel level, bool always_use_real_rng) noexcept { // Make sure the RNG is initialized first (as all Seed* function possibly need hwrand to be available). RNGState& rng = GetRNGState(); @@ -625,24 +648,40 @@ static void ProcRand(unsigned char* out, int num, RNGLevel level) noexcept } // Combine with and update state - if (!rng.MixExtract(out, num, std::move(hasher), false)) { + if (!rng.MixExtract(out, num, std::move(hasher), false, always_use_real_rng)) { // On the first invocation, also seed with SeedStartup(). CSHA512 startup_hasher; SeedStartup(startup_hasher, rng); - rng.MixExtract(out, num, std::move(startup_hasher), true); + rng.MixExtract(out, num, std::move(startup_hasher), true, always_use_real_rng); } } -void GetRandBytes(Span bytes) noexcept { ProcRand(bytes.data(), bytes.size(), RNGLevel::FAST); } -void GetStrongRandBytes(Span bytes) noexcept { ProcRand(bytes.data(), bytes.size(), RNGLevel::SLOW); } -void RandAddPeriodic() noexcept { ProcRand(nullptr, 0, RNGLevel::PERIODIC); } -void RandAddEvent(const uint32_t event_info) noexcept { GetRNGState().AddEvent(event_info); } +/** Internal function to set g_determinstic_rng. Only accessed from tests. */ +void MakeRandDeterministicDANGEROUS(const uint256& seed) noexcept +{ + GetRNGState().MakeDeterministic(seed); +} -bool g_mock_deterministic_tests{false}; +void GetRandBytes(Span bytes) noexcept +{ + ProcRand(bytes.data(), bytes.size(), RNGLevel::FAST, /*always_use_real_rng=*/false); +} + +void GetStrongRandBytes(Span bytes) noexcept +{ + ProcRand(bytes.data(), bytes.size(), RNGLevel::SLOW, /*always_use_real_rng=*/true); +} + +void RandAddPeriodic() noexcept +{ + ProcRand(nullptr, 0, RNGLevel::PERIODIC, /*always_use_real_rng=*/false); +} + +void RandAddEvent(const uint32_t event_info) noexcept { GetRNGState().AddEvent(event_info); } uint64_t GetRandInternal(uint64_t nMax) noexcept { - return FastRandomContext(g_mock_deterministic_tests).randrange(nMax); + return FastRandomContext().randrange(nMax); } uint256 GetRandHash() noexcept @@ -708,7 +747,7 @@ bool Random_SanityCheck() CSHA512 to_add; to_add.Write((const unsigned char*)&start, sizeof(start)); to_add.Write((const unsigned char*)&stop, sizeof(stop)); - GetRNGState().MixExtract(nullptr, 0, std::move(to_add), false); + GetRNGState().MixExtract(nullptr, 0, std::move(to_add), false, /*always_use_real_rng=*/true); return true; } @@ -734,7 +773,7 @@ FastRandomContext& FastRandomContext::operator=(FastRandomContext&& from) noexce void RandomInit() { // Invoke RNG code to trigger initialization (if not already performed) - ProcRand(nullptr, 0, RNGLevel::FAST); + ProcRand(nullptr, 0, RNGLevel::FAST, /*always_use_real_rng=*/true); ReportHardwareRand(); } diff --git a/src/random.h b/src/random.h index cdf2f3a66d2..fb83eebbf2f 100644 --- a/src/random.h +++ b/src/random.h @@ -63,6 +63,12 @@ * When mixing in new entropy, H = SHA512(entropy || old_rng_state) is computed, and * (up to) the first 32 bytes of H are produced as output, while the last 32 bytes * become the new RNG state. + * + * During tests, the RNG can be put into a special deterministic mode, in which the output + * of all RNG functions, with the exception of GetStrongRandBytes(), is replaced with the + * output of a deterministic RNG. This deterministic RNG does not gather entropy, and is + * unaffected by RandAddPeriodic() or RandAddEvent(). It produces pseudorandom data that + * only depends on the seed it was initialized with, possibly until it is reinitialized. */ /** @@ -340,6 +346,7 @@ private: void RandomSeed() noexcept; public: + /** Construct a FastRandomContext with GetRandHash()-based entropy (or zero key if fDeterministic). */ explicit FastRandomContext(bool fDeterministic = false) noexcept; /** Initialize with explicit seed (only for testing) */ diff --git a/src/test/bloom_tests.cpp b/src/test/bloom_tests.cpp index cbf85277a86..6699afdbfab 100644 --- a/src/test/bloom_tests.cpp +++ b/src/test/bloom_tests.cpp @@ -463,8 +463,7 @@ static std::vector RandomData() BOOST_AUTO_TEST_CASE(rolling_bloom) { - SeedInsecureRand(SeedRand::ZEROS); - g_mock_deterministic_tests = true; + SeedRandomForTest(SeedRand::ZEROS); // last-100-entry, 1% false positive: CRollingBloomFilter rb1(100, 0.01); @@ -491,7 +490,7 @@ BOOST_AUTO_TEST_CASE(rolling_bloom) ++nHits; } // Expect about 100 hits - BOOST_CHECK_EQUAL(nHits, 75U); + BOOST_CHECK_EQUAL(nHits, 71U); BOOST_CHECK(rb1.contains(data[DATASIZE-1])); rb1.reset(); @@ -519,7 +518,7 @@ BOOST_AUTO_TEST_CASE(rolling_bloom) ++nHits; } // Expect about 5 false positives - BOOST_CHECK_EQUAL(nHits, 6U); + BOOST_CHECK_EQUAL(nHits, 3U); // last-1000-entry, 0.01% false positive: CRollingBloomFilter rb2(1000, 0.001); @@ -530,7 +529,6 @@ BOOST_AUTO_TEST_CASE(rolling_bloom) for (int i = 0; i < DATASIZE; i++) { BOOST_CHECK(rb2.contains(data[i])); } - g_mock_deterministic_tests = false; } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/coins_tests.cpp b/src/test/coins_tests.cpp index b6d3e7d5676..a992e2fa033 100644 --- a/src/test/coins_tests.cpp +++ b/src/test/coins_tests.cpp @@ -307,8 +307,7 @@ UtxoData::iterator FindRandomFrom(const std::set &utxoSet) { // has the expected effect (the other duplicate is overwritten at all cache levels) BOOST_AUTO_TEST_CASE(updatecoins_simulation_test) { - SeedInsecureRand(SeedRand::ZEROS); - g_mock_deterministic_tests = true; + SeedRandomForTest(SeedRand::ZEROS); bool spent_a_duplicate_coinbase = false; // A simple map to track what we expect the cache stack to represent. @@ -496,8 +495,6 @@ BOOST_AUTO_TEST_CASE(updatecoins_simulation_test) // Verify coverage. BOOST_CHECK(spent_a_duplicate_coinbase); - - g_mock_deterministic_tests = false; } BOOST_AUTO_TEST_CASE(ccoins_serialization) diff --git a/src/test/cuckoocache_tests.cpp b/src/test/cuckoocache_tests.cpp index eafbcf5681d..906fbb4afa5 100644 --- a/src/test/cuckoocache_tests.cpp +++ b/src/test/cuckoocache_tests.cpp @@ -37,7 +37,7 @@ BOOST_AUTO_TEST_SUITE(cuckoocache_tests); */ BOOST_AUTO_TEST_CASE(test_cuckoocache_no_fakes) { - SeedInsecureRand(SeedRand::ZEROS); + SeedRandomForTest(SeedRand::ZEROS); CuckooCache::cache cc{}; size_t megabytes = 4; cc.setup_bytes(megabytes << 20); @@ -55,7 +55,7 @@ BOOST_AUTO_TEST_CASE(test_cuckoocache_no_fakes) template static double test_cache(size_t megabytes, double load) { - SeedInsecureRand(SeedRand::ZEROS); + SeedRandomForTest(SeedRand::ZEROS); std::vector hashes; Cache set{}; size_t bytes = megabytes * (1 << 20); @@ -126,7 +126,7 @@ template static void test_cache_erase(size_t megabytes) { double load = 1; - SeedInsecureRand(SeedRand::ZEROS); + SeedRandomForTest(SeedRand::ZEROS); std::vector hashes; Cache set{}; size_t bytes = megabytes * (1 << 20); @@ -189,7 +189,7 @@ template static void test_cache_erase_parallel(size_t megabytes) { double load = 1; - SeedInsecureRand(SeedRand::ZEROS); + SeedRandomForTest(SeedRand::ZEROS); std::vector hashes; Cache set{}; size_t bytes = megabytes * (1 << 20); @@ -293,7 +293,7 @@ static void test_cache_generations() // iterations with non-deterministic values, so it isn't "overfit" to the // specific entropy in FastRandomContext(true) and implementation of the // cache. - SeedInsecureRand(SeedRand::ZEROS); + SeedRandomForTest(SeedRand::ZEROS); // block_activity models a chunk of network activity. n_insert elements are // added to the cache. The first and last n/4 are stored for removal later diff --git a/src/test/fuzz/fuzz.cpp b/src/test/fuzz/fuzz.cpp index c1c9945a04a..115cf2fc997 100644 --- a/src/test/fuzz/fuzz.cpp +++ b/src/test/fuzz/fuzz.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include diff --git a/src/test/prevector_tests.cpp b/src/test/prevector_tests.cpp index 1559011fcd1..9abdd84c5a4 100644 --- a/src/test/prevector_tests.cpp +++ b/src/test/prevector_tests.cpp @@ -210,7 +210,7 @@ public: } prevector_tester() { - SeedInsecureRand(); + SeedRandomForTest(); rand_seed = InsecureRand256(); rand_cache = FastRandomContext(rand_seed); } diff --git a/src/test/random_tests.cpp b/src/test/random_tests.cpp index 8392a2bc5d1..89546166b4b 100644 --- a/src/test/random_tests.cpp +++ b/src/test/random_tests.cpp @@ -20,19 +20,30 @@ BOOST_AUTO_TEST_CASE(osrandom_tests) BOOST_CHECK(Random_SanityCheck()); } -BOOST_AUTO_TEST_CASE(fastrandom_tests) +BOOST_AUTO_TEST_CASE(fastrandom_tests_deterministic) { // Check that deterministic FastRandomContexts are deterministic - g_mock_deterministic_tests = true; - FastRandomContext ctx1(true); - FastRandomContext ctx2(true); + SeedRandomForTest(SeedRand::ZEROS); + FastRandomContext ctx1{true}; + FastRandomContext ctx2{true}; - for (int i = 10; i > 0; --i) { - BOOST_CHECK_EQUAL(GetRand(), uint64_t{10393729187455219830U}); - BOOST_CHECK_EQUAL(GetRand(), int{769702006}); - BOOST_CHECK_EQUAL(GetRandMicros(std::chrono::hours{1}).count(), 2917185654); - BOOST_CHECK_EQUAL(GetRandMillis(std::chrono::hours{1}).count(), 2144374); + { + BOOST_CHECK_EQUAL(GetRand(), uint64_t{9330418229102544152u}); + BOOST_CHECK_EQUAL(GetRand(), int{618925161}); + BOOST_CHECK_EQUAL(GetRandMicros(std::chrono::hours{1}).count(), 1271170921); + BOOST_CHECK_EQUAL(GetRandMillis(std::chrono::hours{1}).count(), 2803534); + + BOOST_CHECK_EQUAL(GetRand(), uint64_t{10170981140880778086u}); + BOOST_CHECK_EQUAL(GetRand(), int{1689082725}); + BOOST_CHECK_EQUAL(GetRandMicros(std::chrono::hours{1}).count(), 2464643716); + BOOST_CHECK_EQUAL(GetRandMillis(std::chrono::hours{1}).count(), 2312205); + + BOOST_CHECK_EQUAL(GetRand(), uint64_t{5689404004456455543u}); + BOOST_CHECK_EQUAL(GetRand(), int{785839937}); + BOOST_CHECK_EQUAL(GetRandMicros(std::chrono::hours{1}).count(), 93558804); + BOOST_CHECK_EQUAL(GetRandMillis(std::chrono::hours{1}).count(), 507022); } + { constexpr SteadySeconds time_point{1s}; FastRandomContext ctx{true}; @@ -65,15 +76,28 @@ BOOST_AUTO_TEST_CASE(fastrandom_tests) // Check with time-point type BOOST_CHECK_EQUAL(2782, ctx.rand_uniform_duration(9h).count()); } +} +BOOST_AUTO_TEST_CASE(fastrandom_tests_nondeterministic) +{ // Check that a nondeterministic ones are not - g_mock_deterministic_tests = false; - for (int i = 10; i > 0; --i) { - BOOST_CHECK(GetRand() != uint64_t{10393729187455219830U}); - BOOST_CHECK(GetRand() != int{769702006}); - BOOST_CHECK(GetRandMicros(std::chrono::hours{1}) != std::chrono::microseconds{2917185654}); - BOOST_CHECK(GetRandMillis(std::chrono::hours{1}) != std::chrono::milliseconds{2144374}); + { + BOOST_CHECK(GetRand() != uint64_t{9330418229102544152u}); + BOOST_CHECK(GetRand() != int{618925161}); + BOOST_CHECK(GetRandMicros(std::chrono::hours{1}).count() != 1271170921); + BOOST_CHECK(GetRandMillis(std::chrono::hours{1}).count() != 2803534); + + BOOST_CHECK(GetRand() != uint64_t{10170981140880778086u}); + BOOST_CHECK(GetRand() != int{1689082725}); + BOOST_CHECK(GetRandMicros(std::chrono::hours{1}).count() != 2464643716); + BOOST_CHECK(GetRandMillis(std::chrono::hours{1}).count() != 2312205); + + BOOST_CHECK(GetRand() != uint64_t{5689404004456455543u}); + BOOST_CHECK(GetRand() != int{785839937}); + BOOST_CHECK(GetRandMicros(std::chrono::hours{1}).count() != 93558804); + BOOST_CHECK(GetRandMillis(std::chrono::hours{1}).count() != 507022); } + { FastRandomContext ctx3, ctx4; BOOST_CHECK(ctx3.rand64() != ctx4.rand64()); // extremely unlikely to be equal diff --git a/src/test/streams_tests.cpp b/src/test/streams_tests.cpp index e666e117582..eed932b6d29 100644 --- a/src/test/streams_tests.cpp +++ b/src/test/streams_tests.cpp @@ -436,7 +436,7 @@ BOOST_AUTO_TEST_CASE(streams_buffered_file_skip) BOOST_AUTO_TEST_CASE(streams_buffered_file_rand) { // Make this test deterministic. - SeedInsecureRand(SeedRand::ZEROS); + SeedRandomForTest(SeedRand::ZEROS); fs::path streams_test_filename = m_args.GetDataDirBase() / "streams_test_tmp"; for (int rep = 0; rep < 50; ++rep) { diff --git a/src/test/util/random.cpp b/src/test/util/random.cpp index 4c87ab8df84..aa8c16e8377 100644 --- a/src/test/util/random.cpp +++ b/src/test/util/random.cpp @@ -13,21 +13,26 @@ FastRandomContext g_insecure_rand_ctx; -/** Return the unsigned from the environment var if available, otherwise 0 */ -static uint256 GetUintFromEnv(const std::string& env_name) -{ - const char* num = std::getenv(env_name.c_str()); - if (!num) return {}; - return uint256S(num); -} +extern void MakeRandDeterministicDANGEROUS(const uint256& seed) noexcept; -void Seed(FastRandomContext& ctx) +void SeedRandomForTest(SeedRand seedtype) { - // Should be enough to get the seed once for the process - static uint256 seed{}; static const std::string RANDOM_CTX_SEED{"RANDOM_CTX_SEED"}; - if (seed.IsNull()) seed = GetUintFromEnv(RANDOM_CTX_SEED); - if (seed.IsNull()) seed = GetRandHash(); + + // Do this once, on the first call, regardless of seedtype, because once + // MakeRandDeterministicDANGEROUS is called, the output of GetRandHash is + // no longer truly random. It should be enough to get the seed once for the + // process. + static const uint256 ctx_seed = []() { + // If RANDOM_CTX_SEED is set, use that as seed. + const char* num = std::getenv(RANDOM_CTX_SEED.c_str()); + if (num) return uint256S(num); + // Otherwise use a (truly) random value. + return GetRandHash(); + }(); + + const uint256& seed{seedtype == SeedRand::SEED ? ctx_seed : uint256::ZERO}; LogPrintf("%s: Setting random seed for current tests to %s=%s\n", __func__, RANDOM_CTX_SEED, seed.GetHex()); - ctx = FastRandomContext(seed); + MakeRandDeterministicDANGEROUS(seed); + g_insecure_rand_ctx = FastRandomContext(GetRandHash()); } diff --git a/src/test/util/random.h b/src/test/util/random.h index 18ab425e481..09a475f8b3d 100644 --- a/src/test/util/random.h +++ b/src/test/util/random.h @@ -19,27 +19,13 @@ */ extern FastRandomContext g_insecure_rand_ctx; -/** - * Flag to make GetRand in random.h return the same number - */ -extern bool g_mock_deterministic_tests; - enum class SeedRand { ZEROS, //!< Seed with a compile time constant of zeros - SEED, //!< Call the Seed() helper + SEED, //!< Use (and report) random seed from environment, or a (truly) random one. }; -/** Seed the given random ctx or use the seed passed in via an environment var */ -void Seed(FastRandomContext& ctx); - -static inline void SeedInsecureRand(SeedRand seed = SeedRand::SEED) -{ - if (seed == SeedRand::ZEROS) { - g_insecure_rand_ctx = FastRandomContext(/*fDeterministic=*/true); - } else { - Seed(g_insecure_rand_ctx); - } -} +/** Seed the RNG for testing. This affects all randomness, except GetStrongRandBytes(). */ +void SeedRandomForTest(SeedRand seed = SeedRand::SEED); static inline uint32_t InsecureRand32() { diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp index cc7b2d65463..283e19971c0 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -178,7 +178,7 @@ BasicTestingSetup::BasicTestingSetup(const ChainType chainType, const std::vecto gArgs.ForceSetArg("-datadir", fs::PathToString(m_path_root)); SelectParams(chainType); - SeedInsecureRand(); + SeedRandomForTest(); if (G_TEST_LOG_FUN) LogInstance().PushBackCallback(G_TEST_LOG_FUN); InitLogging(*m_node.args); AppInitParameterInteraction(*m_node.args); diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp index a371753adf0..9f452d5f8f2 100644 --- a/src/test/util_tests.cpp +++ b/src/test/util_tests.cpp @@ -459,7 +459,7 @@ BOOST_AUTO_TEST_CASE(util_IsHexNumber) BOOST_AUTO_TEST_CASE(util_seed_insecure_rand) { - SeedInsecureRand(SeedRand::ZEROS); + SeedRandomForTest(SeedRand::ZEROS); for (int mod=2;mod<11;mod++) { int mask = 1;