Merge bitcoin/bitcoin#34328: rpc: make uptime monotonic across NTP jumps

14f99cfe53f07280b6f047844fc4fba0da8cd328 rpc: make `uptime` monotonic across NTP jumps (Lőrinc)
a9440b1595be7053b17895f7ee36652bac24be6e util: add `TicksSeconds` (Lőrinc)

Pull request description:

  ### Problem
  `bitcoin-cli uptime` was derived from wall-clock time, so it could jump by large amounts when the system clock is corrected after `bitcoind` starts (e.g. on RTC-less systems syncing NTP).
  This breaks the expectation that uptime reflects process runtime.

  ### Fix
  Compute uptime from a [monotonic clock](https://en.cppreference.com/w/cpp/chrono/steady_clock.html) so it is immune to wall-clock jumps, and use that monotonic uptime for the RPC.
  GUI startup time is derived from wall clock time minus monotonic uptime so it remains sensible after clock corrections.

  ### Reproducer
  Revert the fix commit and run the `rpc_uptime` functional test (it should fail with `AssertionError: uptime should not jump with wall clock`):

  Or alternatively:

  ```bash
  cmake -B build && cmake --build build --target bitcoind bitcoin-cli -j$(nproc)
  DATA_DIR=$(mktemp -d)
  ./build/bin/bitcoind -regtest -datadir="$DATA_DIR" -connect=0 -daemon
  ./build/bin/bitcoin-cli -regtest -datadir="$DATA_DIR" -rpcwait uptime
  sleep 1
  ./build/bin/bitcoin-cli -regtest -datadir="$DATA_DIR" setmocktime $(( $(date +%s) + 20000000 ))
  ./build/bin/bitcoin-cli -regtest -datadir="$DATA_DIR" uptime
  ./build/bin/bitcoin-cli -regtest -datadir="$DATA_DIR" stop
  ```

  <details>
  <summary>Before (uptime jumps with wall clock)</summary>

  ```bash
  Bitcoin Core starting
  0
  20000001
  Bitcoin Core stopping
  ```

  </details>

  <details>
  <summary>After (uptime stays monotonic)</summary>

  ```bash
  Bitcoin Core starting
  0
  1
  Bitcoin Core stopping
  ```
  </details>

  ----------

  Issue: https://github.com/bitcoin/bitcoin/issues/34326

ACKs for top commit:
  maflcko:
    review ACK 14f99cfe53f07280b6f047844fc4fba0da8cd328 🎦
  willcl-ark:
    tACK 14f99cfe53f07280b6f047844fc4fba0da8cd328
  w0xlt:
    ACK 14f99cfe53f07280b6f047844fc4fba0da8cd328
  sedited:
    ACK 14f99cfe53f07280b6f047844fc4fba0da8cd328

Tree-SHA512: 3909973f58666ffa0b784a6df087031b9e34d2022d354900a4dbb6cbe1d36285cd92770ee71350ebf64d6e8ab212d8ff0cd851f7dca1ec46ee2f19b417f53984
This commit is contained in:
merge-script 2026-01-27 13:26:43 +01:00
commit 27aeeff630
No known key found for this signature in database
GPG Key ID: 9B79B45691DB4173
7 changed files with 30 additions and 13 deletions

View File

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

View File

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

View File

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

View File

@ -184,7 +184,7 @@ static RPCHelpMan uptime()
},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
return GetTime() - GetStartupTime();
return TicksSeconds(GetUptime());
}
};
}

View File

@ -600,6 +600,15 @@ BOOST_AUTO_TEST_CASE(util_mocktime)
SetMockTime(0s);
}
BOOST_AUTO_TEST_CASE(util_ticksseconds)
{
BOOST_CHECK_EQUAL(TicksSeconds(0s), 0);
BOOST_CHECK_EQUAL(TicksSeconds(1s), 1);
BOOST_CHECK_EQUAL(TicksSeconds(999ms), 0);
BOOST_CHECK_EQUAL(TicksSeconds(1000ms), 1);
BOOST_CHECK_EQUAL(TicksSeconds(1500ms), 1);
}
BOOST_AUTO_TEST_CASE(test_IsDigit)
{
BOOST_CHECK_EQUAL(IsDigit('0'), true);

View File

@ -74,6 +74,12 @@ constexpr auto Ticks(Dur2 d)
{
return std::chrono::duration_cast<Dur1>(d).count();
}
template <typename Duration>
constexpr int64_t TicksSeconds(Duration d)
{
return int64_t{Ticks<std::chrono::seconds>(d)};
}
template <typename Duration, typename Timepoint>
constexpr auto TicksSinceEpoch(Timepoint t)
{

View File

@ -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__':