From 88401bc25e10e38f6fa77bc036b95bfbb23053e6 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Tue, 28 Jun 2022 17:18:36 -0600 Subject: [PATCH] Add a clock for testing with an offset from the system clock. This change improves clock management for zcashd by ensuring that all clock methods (obtaining seconds, milliseconds, and microseconds since the epoch) agree under testing conditions using `-mocktime`, and also adds a feature that allows tests to specify an offset to the system clock; this is useful to allow comprehensive testing of the "timejacking attack mitigation" consensus rules. --- src/gtest/test_mempoollimit.cpp | 25 +++++---- src/gtest/test_metrics.cpp | 23 ++++---- src/init.cpp | 13 ++++- src/rpc/misc.cpp | 7 ++- src/test/DoS_tests.cpp | 8 +-- src/test/alert_tests.cpp | 12 ++--- src/test/checkblock_tests.cpp | 4 +- src/test/miner_tests.cpp | 8 +-- src/utiltime.cpp | 80 +++++++++++++++++++++++----- src/utiltime.h | 94 ++++++++++++++++++++++++++++++++- 10 files changed, 222 insertions(+), 52 deletions(-) diff --git a/src/gtest/test_mempoollimit.cpp b/src/gtest/test_mempoollimit.cpp index b674189be..0bd059808 100644 --- a/src/gtest/test_mempoollimit.cpp +++ b/src/gtest/test_mempoollimit.cpp @@ -20,7 +20,7 @@ const uint256 TX_ID3 = ArithToUint256(3); TEST(MempoolLimitTests, RecentlyEvictedListAddWrapsAfterMaxSize) { RecentlyEvictedList recentlyEvicted(2, 100); - SetMockTime(1); + FixedClock::SetGlobal(1); recentlyEvicted.add(TX_ID1); recentlyEvicted.add(TX_ID2); recentlyEvicted.add(TX_ID3); @@ -28,56 +28,59 @@ TEST(MempoolLimitTests, RecentlyEvictedListAddWrapsAfterMaxSize) EXPECT_FALSE(recentlyEvicted.contains(TX_ID1)); EXPECT_TRUE(recentlyEvicted.contains(TX_ID2)); EXPECT_TRUE(recentlyEvicted.contains(TX_ID3)); + SystemClock::SetGlobal(); } TEST(MempoolLimitTests, RecentlyEvictedListDoesNotContainAfterExpiry) { - SetMockTime(1); + FixedClock::SetGlobal(1); // maxSize=3, timeToKeep=1 RecentlyEvictedList recentlyEvicted(3, 1); recentlyEvicted.add(TX_ID1); - SetMockTime(2); + FixedClock::SetGlobal(2); recentlyEvicted.add(TX_ID2); recentlyEvicted.add(TX_ID3); // After 1 second the txId will still be there EXPECT_TRUE(recentlyEvicted.contains(TX_ID1)); EXPECT_TRUE(recentlyEvicted.contains(TX_ID2)); EXPECT_TRUE(recentlyEvicted.contains(TX_ID3)); - SetMockTime(3); + FixedClock::SetGlobal(3); // After 2 seconds it is gone EXPECT_FALSE(recentlyEvicted.contains(TX_ID1)); EXPECT_TRUE(recentlyEvicted.contains(TX_ID2)); EXPECT_TRUE(recentlyEvicted.contains(TX_ID3)); - SetMockTime(4); + FixedClock::SetGlobal(4); EXPECT_FALSE(recentlyEvicted.contains(TX_ID1)); EXPECT_FALSE(recentlyEvicted.contains(TX_ID2)); EXPECT_FALSE(recentlyEvicted.contains(TX_ID3)); + SystemClock::SetGlobal(); } TEST(MempoolLimitTests, RecentlyEvictedDropOneAtATime) { - SetMockTime(1); + FixedClock::SetGlobal(1); RecentlyEvictedList recentlyEvicted(3, 2); recentlyEvicted.add(TX_ID1); - SetMockTime(2); + FixedClock::SetGlobal(2); recentlyEvicted.add(TX_ID2); - SetMockTime(3); + FixedClock::SetGlobal(3); recentlyEvicted.add(TX_ID3); EXPECT_TRUE(recentlyEvicted.contains(TX_ID1)); EXPECT_TRUE(recentlyEvicted.contains(TX_ID2)); EXPECT_TRUE(recentlyEvicted.contains(TX_ID3)); - SetMockTime(4); + FixedClock::SetGlobal(4); EXPECT_FALSE(recentlyEvicted.contains(TX_ID1)); EXPECT_TRUE(recentlyEvicted.contains(TX_ID2)); EXPECT_TRUE(recentlyEvicted.contains(TX_ID3)); - SetMockTime(5); + FixedClock::SetGlobal(5); EXPECT_FALSE(recentlyEvicted.contains(TX_ID1)); EXPECT_FALSE(recentlyEvicted.contains(TX_ID2)); EXPECT_TRUE(recentlyEvicted.contains(TX_ID3)); - SetMockTime(6); + FixedClock::SetGlobal(6); EXPECT_FALSE(recentlyEvicted.contains(TX_ID1)); EXPECT_FALSE(recentlyEvicted.contains(TX_ID2)); EXPECT_FALSE(recentlyEvicted.contains(TX_ID3)); + SystemClock::SetGlobal(); } TEST(MempoolLimitTests, WeightedTxTreeCheckSizeAfterDropping) diff --git a/src/gtest/test_metrics.cpp b/src/gtest/test_metrics.cpp index adeb857d0..abcfd2743 100644 --- a/src/gtest/test_metrics.cpp +++ b/src/gtest/test_metrics.cpp @@ -7,7 +7,7 @@ TEST(Metrics, AtomicTimer) { AtomicTimer t; - SetMockTime(100); + FixedClock::SetGlobal(100); EXPECT_FALSE(t.running()); @@ -36,13 +36,13 @@ TEST(Metrics, AtomicTimer) { c.increment(); EXPECT_EQ(0, t.rate(c)); - SetMockTime(101); + FixedClock::SetGlobal(101); EXPECT_EQ(1, t.rate(c)); c.decrement(); EXPECT_EQ(0, t.rate(c)); - SetMockTime(102); + FixedClock::SetGlobal(102); EXPECT_EQ(0, t.rate(c)); c.increment(); @@ -51,17 +51,19 @@ TEST(Metrics, AtomicTimer) { t.stop(); EXPECT_FALSE(t.running()); EXPECT_EQ(0.5, t.rate(c)); + + SystemClock::SetGlobal(); } TEST(Metrics, GetLocalSolPS) { - SetMockTime(100); + FixedClock::SetGlobal(100); miningTimer.start(); // No time has passed EXPECT_EQ(0, GetLocalSolPS()); // Increment time - SetMockTime(101); + FixedClock::SetGlobal(101); EXPECT_EQ(0, GetLocalSolPS()); // Increment solutions @@ -69,7 +71,7 @@ TEST(Metrics, GetLocalSolPS) { EXPECT_EQ(1, GetLocalSolPS()); // Increment time - SetMockTime(102); + FixedClock::SetGlobal(102); EXPECT_EQ(0.5, GetLocalSolPS()); // Increment solutions @@ -82,7 +84,7 @@ TEST(Metrics, GetLocalSolPS) { EXPECT_EQ(1.5, GetLocalSolPS()); // Increment time - SetMockTime(103); + FixedClock::SetGlobal(103); EXPECT_EQ(1.5, GetLocalSolPS()); // Start timing again @@ -90,7 +92,7 @@ TEST(Metrics, GetLocalSolPS) { EXPECT_EQ(1.5, GetLocalSolPS()); // Increment time - SetMockTime(104); + FixedClock::SetGlobal(104); EXPECT_EQ(1, GetLocalSolPS()); miningTimer.stop(); @@ -98,6 +100,8 @@ TEST(Metrics, GetLocalSolPS) { solutionTargetChecks.decrement(); solutionTargetChecks.decrement(); solutionTargetChecks.decrement(); + + SystemClock::SetGlobal(); } TEST(Metrics, EstimateNetHeight) { @@ -106,11 +110,12 @@ TEST(Metrics, EstimateNetHeight) { for (int i = 0; i < 400; i++) { blockTimes[i] = i ? blockTimes[i - 1] + params.PoWTargetSpacing(i) : 0; } - SetMockTime(blockTimes[399]); + FixedClock::SetGlobal(blockTimes[399]); for (int i = 0; i < 400; i++) { // Check that we are within 1 of the correct height EXPECT_LT(std::abs(399 - EstimateNetHeight(params, i, blockTimes[i])), 2); } + SystemClock::SetGlobal(); RegtestDeactivateBlossom(); } diff --git a/src/init.cpp b/src/init.cpp index 39eb438e2..3cb45e0da 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1240,8 +1240,17 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler) fAlerts = GetBoolArg("-alerts", DEFAULT_ALERTS); - // Option to startup with mocktime set (used for regression testing): - SetMockTime(GetArg("-mocktime", 0)); // SetMockTime(0) is a no-op + // Option to startup with mocktime set (used for regression testing); + // an mocktime of 0 (the default) selects the system clock. + int64_t nMockTime = GetArg("-mocktime", 0); + if (nMockTime != 0) { + FixedClock::SetGlobal(nMockTime); + } else { + // Option to start a node with the system clock offset by a constant + // value throughout the life of the node (used for regression testing): + int64_t nOffsetTime = GetArg("-clockoffset", 0); + OffsetClock::SetGlobal(nOffsetTime); + } if (GetBoolArg("-peerbloomfilters", DEFAULT_PEERBLOOMFILTERS)) nLocalServices |= NODE_BLOOM; diff --git a/src/rpc/misc.cpp b/src/rpc/misc.cpp index ed7a0129c..42a2fbef7 100644 --- a/src/rpc/misc.cpp +++ b/src/rpc/misc.cpp @@ -513,7 +513,12 @@ UniValue setmocktime(const UniValue& params, bool fHelp) LOCK2(cs_main, cs_vNodes); RPCTypeCheck(params, boost::assign::list_of(UniValue::VNUM)); - SetMockTime(params[0].get_int64()); + int64_t nMockTime = params[0].get_int64(); + if (nMockTime == 0) { + SystemClock::SetGlobal(); + } else { + FixedClock::SetGlobal(nMockTime); + } uint64_t t = GetTime(); for (CNode* pnode : vNodes) { diff --git a/src/test/DoS_tests.cpp b/src/test/DoS_tests.cpp index c67b3aa75..0cec76f8a 100644 --- a/src/test/DoS_tests.cpp +++ b/src/test/DoS_tests.cpp @@ -96,7 +96,7 @@ BOOST_AUTO_TEST_CASE(DoS_bantime) const Consensus::Params& params = Params().GetConsensus(); CNode::ClearBanned(); int64_t nStartTime = GetTime(); - SetMockTime(nStartTime); // Overrides future calls to GetTime() + FixedClock::SetGlobal(nStartTime); // Overrides future calls to GetTime() CAddress addr(ip(0xa0b0c001)); CNode dummyNode(INVALID_SOCKET, addr, "", true); @@ -106,11 +106,13 @@ BOOST_AUTO_TEST_CASE(DoS_bantime) SendMessages(params, &dummyNode); BOOST_CHECK(CNode::IsBanned(addr)); - SetMockTime(nStartTime+60*60); + FixedClock::SetGlobal(nStartTime+60*60); BOOST_CHECK(CNode::IsBanned(addr)); - SetMockTime(nStartTime+60*60*24+1); + FixedClock::SetGlobal(nStartTime+60*60*24+1); BOOST_CHECK(!CNode::IsBanned(addr)); + + SystemClock::SetGlobal(); } CTransaction RandomOrphan() diff --git a/src/test/alert_tests.cpp b/src/test/alert_tests.cpp index 15991dc02..e7f610fef 100644 --- a/src/test/alert_tests.cpp +++ b/src/test/alert_tests.cpp @@ -289,7 +289,7 @@ BOOST_FIXTURE_TEST_SUITE(Alert_tests, ReadAlerts) BOOST_AUTO_TEST_CASE(AlertApplies) { - SetMockTime(11); + FixedClock::SetGlobal(11); const std::vector& alertKey = Params(CBaseChainParams::MAIN).AlertKey(); for (const CAlert& alert : alerts) @@ -342,13 +342,13 @@ BOOST_AUTO_TEST_CASE(AlertApplies) // SubVer without comment doesn't match SubVer pattern with BOOST_CHECK(!alerts[3].AppliesTo(1, "/MagicBean:0.2.1/")); - SetMockTime(0); + SystemClock::SetGlobal(); } BOOST_AUTO_TEST_CASE(AlertNotify) { - SetMockTime(11); + FixedClock::SetGlobal(11); const std::vector& alertKey = Params(CBaseChainParams::MAIN).AlertKey(); fs::path temp = fs::temp_directory_path() / @@ -382,13 +382,13 @@ BOOST_AUTO_TEST_CASE(AlertNotify) #endif fs::remove(temp); - SetMockTime(0); + SystemClock::SetGlobal(); mapAlerts.clear(); } BOOST_AUTO_TEST_CASE(AlertDisablesRPC) { - SetMockTime(11); + FixedClock::SetGlobal(11); const std::vector& alertKey = Params(CBaseChainParams::MAIN).AlertKey(); // Command should work before alerts @@ -404,7 +404,7 @@ BOOST_AUTO_TEST_CASE(AlertDisablesRPC) BOOST_CHECK_EQUAL(alerts[8].strRPCError, ""); BOOST_CHECK_EQUAL(GetWarnings("rpc").first, ""); - SetMockTime(0); + SystemClock::SetGlobal(); mapAlerts.clear(); } diff --git a/src/test/checkblock_tests.cpp b/src/test/checkblock_tests.cpp index 77de7fb3a..b7291e422 100644 --- a/src/test/checkblock_tests.cpp +++ b/src/test/checkblock_tests.cpp @@ -48,7 +48,7 @@ BOOST_AUTO_TEST_CASE(May15) // test/data/Mar12Fork.dat from // http://sourceforge.net/projects/bitcoin/files/Bitcoin/blockchain/Mar12Fork.dat/download unsigned int tMay15 = 1368576000; - SetMockTime(tMay15); // Test as if it was right at May 15 + FixedClock::SetGlobal(tMay15); // Test as if it was right at May 15 CBlock forkingBlock; if (read_block("Mar12Fork.dat", forkingBlock)) @@ -61,7 +61,7 @@ BOOST_AUTO_TEST_CASE(May15) BOOST_CHECK(CheckBlock(forkingBlock, state, Params(), verifier, false, false, true)); } - SetMockTime(0); + SystemClock::SetGlobal(); } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/miner_tests.cpp b/src/test/miner_tests.cpp index 6c9b76dea..38a6d2f5f 100644 --- a/src/test/miner_tests.cpp +++ b/src/test/miner_tests.cpp @@ -253,7 +253,7 @@ BOOST_AUTO_TEST_CASE(CreateNewBlock_validity) pblock->nSolution = soln; CValidationState state; - + if (ProcessNewBlock(state, NULL, pblock, true, NULL) && state.IsValid()) { goto foundit; } @@ -408,7 +408,7 @@ BOOST_AUTO_TEST_CASE(CreateNewBlock_validity) chainActive.Tip()->nHeight = nHeight; // non-final txs in mempool - SetMockTime(chainActive.Tip()->GetMedianTimePast()+1); + FixedClock::SetGlobal(chainActive.Tip()->GetMedianTimePast()+1); // height locked tx.vin[0].prevout.hash = txFirst[0]->GetHash(); @@ -443,7 +443,7 @@ BOOST_AUTO_TEST_CASE(CreateNewBlock_validity) // However if we advance height and time by one, both will. chainActive.Tip()->nHeight++; - SetMockTime(chainActive.Tip()->GetMedianTimePast()+2); + FixedClock::SetGlobal(chainActive.Tip()->GetMedianTimePast()+2); // FIXME: we should *actually* create a new block so the following test // works; CheckFinalTx() isn't fooled by monkey-patching nHeight. @@ -455,7 +455,7 @@ BOOST_AUTO_TEST_CASE(CreateNewBlock_validity) delete pblocktemplate; chainActive.Tip()->nHeight--; - SetMockTime(0); + SystemClock::SetGlobal(); mempool.clear(); for (CTransaction *tx : txFirst) diff --git a/src/utiltime.cpp b/src/utiltime.cpp index d843108eb..3c93526de 100644 --- a/src/utiltime.cpp +++ b/src/utiltime.cpp @@ -9,6 +9,7 @@ #endif #include "utiltime.h" +#include "sync.h" #include #include @@ -16,32 +17,85 @@ using namespace std; -static int64_t nMockTime = 0; //!< For unit testing +RecursiveMutex clock_lock; +static CClock* zcashdClock = SystemClock::Instance(); -int64_t GetTime() -{ - if (nMockTime) return nMockTime; - - return time(NULL); +void SystemClock::SetGlobal() { + LOCK(clock_lock); + zcashdClock = SystemClock::Instance(); } -void SetMockTime(int64_t nMockTimeIn) -{ - nMockTime = nMockTimeIn; +int64_t SystemClock::GetTime() const { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); } -int64_t GetTimeMillis() -{ +int64_t SystemClock::GetTimeMillis() const { return std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); } -int64_t GetTimeMicros() -{ +int64_t SystemClock::GetTimeMicros() const { return std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); } +void FixedClock::SetGlobal(int64_t nFixedTime) { + LOCK(clock_lock); + FixedClock::Instance()->Set(nFixedTime); + zcashdClock = FixedClock::Instance(); +} + +int64_t FixedClock::GetTime() const { + return nFixedTime; +} + +int64_t FixedClock::GetTimeMillis() const { + return nFixedTime * 1000; +} + +int64_t FixedClock::GetTimeMicros() const { + return nFixedTime * 1000000; +} + +OffsetClock OffsetClock::instance; + +void OffsetClock::SetGlobal(int64_t nOffsetSeconds) { + LOCK(clock_lock); + OffsetClock::Instance()->Set(nOffsetSeconds); + zcashdClock = OffsetClock::Instance(); +} + +int64_t OffsetClock::GetTime() const { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count() + + nOffsetSeconds; +} + +int64_t OffsetClock::GetTimeMillis() const { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count() + + (nOffsetSeconds * 1000); +} + +int64_t OffsetClock::GetTimeMicros() const { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count() + + (nOffsetSeconds * 1000000); +} + +int64_t GetTime() { + return zcashdClock->GetTime(); +} + +int64_t GetTimeMillis() { + return zcashdClock->GetTimeMillis(); +} + +int64_t GetTimeMicros() { + return zcashdClock->GetTimeMicros(); +} + void MilliSleep(int64_t n) { // This is defined to be an interruption point. diff --git a/src/utiltime.h b/src/utiltime.h index 007d41708..821dd372a 100644 --- a/src/utiltime.h +++ b/src/utiltime.h @@ -10,10 +10,102 @@ #include #include +class CClock { +public: + /** Returns the current time in seconds since the POSIX epoch. */ + virtual int64_t GetTime() const = 0; + /** Returns the current time in milliseconds since the POSIX epoch. */ + virtual int64_t GetTimeMillis() const = 0; + /** Returns the current time in microseconds since the POSIX epoch. */ + virtual int64_t GetTimeMicros() const = 0; +}; + +class SystemClock: public CClock { +private: + SystemClock() {} + ~SystemClock() {} + SystemClock(SystemClock const&) = delete; + SystemClock& operator=(const SystemClock&)= delete; +public: + static SystemClock* Instance() { + static SystemClock instance; + return &instance; + } + + /** Sets the clock used by zcashd to the system clock. */ + static void SetGlobal(); + + int64_t GetTime() const; + int64_t GetTimeMillis() const; + int64_t GetTimeMicros() const; +}; + +class FixedClock: public CClock { +private: + static FixedClock instance; + int64_t nFixedTime; + + FixedClock(): nFixedTime(0) {} + ~FixedClock() {} + FixedClock(FixedClock const&) = delete; + FixedClock& operator=(const FixedClock&)= delete; + + void Set(int64_t nFixedTime) { + this->nFixedTime = nFixedTime; + } +public: + static FixedClock* Instance() { + static FixedClock instance; + return &instance; + } + + /** + * Sets the clock used by zcashd to a fixed clock that always + * returns the specified timestamp. + */ + static void SetGlobal(int64_t nFixedTime); + + int64_t GetTime() const; + int64_t GetTimeMillis() const; + int64_t GetTimeMicros() const; +}; + +class OffsetClock: public CClock { +private: + static OffsetClock instance; + int64_t nOffsetSeconds; + + OffsetClock(): nOffsetSeconds(0) {} + ~OffsetClock() {} + OffsetClock(OffsetClock const&) = delete; + OffsetClock& operator=(const OffsetClock&)= delete; + + void Set(int64_t nOffsetSeconds) { + this->nOffsetSeconds = nOffsetSeconds; + } +public: + static OffsetClock* Instance() { + static OffsetClock instance; + return &instance; + } + + /** + * Sets the clock used by zcashd to a clock that returns the current + * system time modified by the specified offset. + */ + static void SetGlobal(int64_t nOffsetSeconds); + + int64_t GetTime() const; + int64_t GetTimeMillis() const; + int64_t GetTimeMicros() const; +}; + +const CClock& GetClock(); + int64_t GetTime(); int64_t GetTimeMillis(); int64_t GetTimeMicros(); -void SetMockTime(int64_t nMockTimeIn); + void MilliSleep(int64_t n); std::string DateTimeStrFormat(const char* pszFormat, int64_t nTime);