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.
This commit is contained in:
Kris Nuttycombe 2022-06-28 17:18:36 -06:00 committed by Daira Hopwood
parent bb64e895c2
commit 88401bc25e
10 changed files with 222 additions and 52 deletions

View File

@ -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)

View File

@ -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();
}

View File

@ -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;

View File

@ -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) {

View File

@ -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()

View File

@ -289,7 +289,7 @@ BOOST_FIXTURE_TEST_SUITE(Alert_tests, ReadAlerts)
BOOST_AUTO_TEST_CASE(AlertApplies)
{
SetMockTime(11);
FixedClock::SetGlobal(11);
const std::vector<unsigned char>& 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<unsigned char>& 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<unsigned char>& 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();
}

View File

@ -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()

View File

@ -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)

View File

@ -9,6 +9,7 @@
#endif
#include "utiltime.h"
#include "sync.h"
#include <chrono>
#include <boost/date_time/posix_time/posix_time.hpp>
@ -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::seconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
}
int64_t GetTimeMillis()
{
int64_t SystemClock::GetTimeMillis() const {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
}
int64_t GetTimeMicros()
{
int64_t SystemClock::GetTimeMicros() const {
return std::chrono::duration_cast<std::chrono::microseconds>(
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::seconds>(
std::chrono::system_clock::now().time_since_epoch()).count()
+ nOffsetSeconds;
}
int64_t OffsetClock::GetTimeMillis() const {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count()
+ (nOffsetSeconds * 1000);
}
int64_t OffsetClock::GetTimeMicros() const {
return std::chrono::duration_cast<std::chrono::microseconds>(
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.

View File

@ -10,10 +10,102 @@
#include <stdint.h>
#include <string>
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);