rpc: make `uptime` monotonic across NTP jumps
What changed, and why it matters
This commit fixes the `uptime` RPC command in Bitcoin Core so it no longer jumps around when the system clock or mock time is changed. Previously, uptime was calculated by subtracting the startup wall-clock time from the current wall-clock time, so a large NTP adjustment or `setmocktime` call could make the node appear to have been running for thousands of seconds instantly, or even report a negative uptime. The change uses a monotonic clock that only counts real elapsed time. It is a correctness/reliability fix, not a direct theft-of-funds vulnerability.
No urgent action required. This is a low-severity reliability fix. Operators and integrators relying on the `uptime` RPC for monitoring should ensure they upgrade to a version containing this commit if they observed anomalous uptime readings after clock adjustments.
Security signals we found
RPC output correctness fix
monotonic clock used instead of wall-clock time
functional test added for time-jump behavior
no cryptographic, consensus, or network-layer changes
Evidence from the diff
The patch replaces GetStartupTime() (a wall-clock Unix timestamp captured at startup) with GetUptime(), which returns SteadyClock::now() - g_startup_time. The uptime RPC now returns TicksSeconds(GetUptime()). The GUI startup-time display is updated to derive the original wall-clock startup time by subtracting the monotonic uptime from the current wall-clock time. A functional test is added that calls setmocktime with a 20,000-second jump and asserts the RPC uptime does not jump by that amount.
Changed components
src/common/system.cppsrc/common/system.hsrc/qt/clientmodel.cppsrc/rpc/server.cpptest/functional/rpc_uptime.pyInspect captured patch +15 / −13
diff --git a/src/common/system.cpp b/src/common/system.cpp
index 72e9de10..98bc0147 100644
--- a/src/common/system.cpp
+++ b/src/common/system.cpp
@@ -37,9 +37,6 @@
using util::ReplaceAll;
-// Application startup time (used for uptime calculation)
-const int64_t nStartupTime = GetTime();
-
#ifndef WIN32
std::string ShellEscape(const std::string& arg)
{
@@ -130,8 +127,8 @@ std::optional<size_t> GetTotalRAM()
return std::nullopt;
}
-// Obtain the application startup time (used for uptime calculation)
-int64_t GetStartupTime()
+SteadyClock::duration GetUptime()
{
- return nStartupTime;
+ static const auto g_startup_time{SteadyClock::now()};
+ return SteadyClock::now() - g_startup_time;
}
diff --git a/src/common/system.h b/src/common/system.h
index 2184f1d4..a3100fec 100644
--- a/src/common/system.h
+++ b/src/common/system.h
@@ -7,13 +7,15 @@
#define BITCOIN_COMMON_SYSTEM_H
#include <bitcoin-build-config.h> // IWYU pragma: keep
+#include <util/time.h>
+#include <chrono>
#include <cstdint>
#include <optional>
#include <string>
-// Application startup time (used for uptime calculation)
-int64_t GetStartupTime();
+/// Monotonic uptime (not affected by system time changes).
+SteadyClock::duration GetUptime();
void SetupEnvironment();
[[nodiscard]] bool SetupNetworking();
diff --git a/src/qt/clientmodel.cpp b/src/qt/clientmodel.cpp
index dda26fa0..cd7d9e32 100644
--- a/src/qt/clientmodel.cpp
+++ b/src/qt/clientmodel.cpp
@@ -210,7 +210,7 @@ bool ClientModel::isReleaseVersion() const
QString ClientModel::formatClientStartupTime() const
{
- return QDateTime::fromSecsSinceEpoch(GetStartupTime()).toString();
+ return QDateTime::currentDateTime().addSecs(-TicksSeconds(GetUptime())).toString();
}
QString ClientModel::dataDir() const
diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp
index 5fd733d0..01fb815a 100644
--- a/src/rpc/server.cpp
+++ b/src/rpc/server.cpp
@@ -184,7 +184,7 @@ static RPCHelpMan uptime()
},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
- return GetTime() - GetStartupTime();
+ return TicksSeconds(GetUptime());
}
};
}
diff --git a/test/functional/rpc_uptime.py b/test/functional/rpc_uptime.py
index 74880417..817ba2b4 100755
--- a/test/functional/rpc_uptime.py
+++ b/test/functional/rpc_uptime.py
@@ -26,9 +26,12 @@ class UptimeTest(BitcoinTestFramework):
assert_raises_rpc_error(-8, "Mocktime must be in the range [0, 9223372036], not -1.", self.nodes[0].setmocktime, -1)
def _test_uptime(self):
- wait_time = 10
- self.nodes[0].setmocktime(int(time.time() + wait_time))
- assert self.nodes[0].uptime() >= wait_time
+ wait_time = 20_000
+ uptime_before = self.nodes[0].uptime()
+ self.nodes[0].setmocktime(int(time.time()) + wait_time)
+ uptime_after = self.nodes[0].uptime()
+ self.nodes[0].setmocktime(0)
+ assert uptime_after - uptime_before < wait_time, "uptime should not jump with wall clock"
if __name__ == '__main__':
Why this scored 21/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.