rpc: make uptime monotonic across NTP jumps #34328

pull l0rinc wants to merge 2 commits into bitcoin:master from l0rinc:l0rinc/uptime-monotonic changing 7 files +30 −13
  1. l0rinc commented at 8:01 pm on January 17, 2026: contributor

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

    0cmake -B build && cmake --build build --target bitcoind bitcoin-cli -j$(nproc)
    1DATA_DIR=$(mktemp -d)
    2./build/bin/bitcoind -regtest -datadir="$DATA_DIR" -connect=0 -daemon
    3./build/bin/bitcoin-cli -regtest -datadir="$DATA_DIR" -rpcwait uptime
    4sleep 1
    5./build/bin/bitcoin-cli -regtest -datadir="$DATA_DIR" setmocktime $(( $(date +%s) + 20000000 ))
    6./build/bin/bitcoin-cli -regtest -datadir="$DATA_DIR" uptime
    7./build/bin/bitcoin-cli -regtest -datadir="$DATA_DIR" stop
    
    0Bitcoin Core starting
    10
    220000001
    3Bitcoin Core stopping
    
    0Bitcoin Core starting
    10
    21
    3Bitcoin Core stopping
    

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

  2. DrahtBot added the label RPC/REST/ZMQ on Jan 17, 2026
  3. DrahtBot commented at 8:01 pm on January 17, 2026: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/34328.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK maflcko, willcl-ark, w0xlt

    If your review is incorrectly listed, please copy-paste <!–meta-tag:bot-skip–> into the comment that the bot should ignore.

    LLM Linter (✨ experimental)

    Possible places where comparison-specific test macros should replace generic comparisons:

    • test/functional/rpc_uptime.py “assert uptime_after - uptime_before < wait_time, "uptime should not jump with wall clock"” -> recommendation: use the comparison helper by reversing operands, e.g. assert_greater_than(wait_time, uptime_after - uptime_before), to avoid a bare assert.

    2026-01-19

  4. in src/qt/clientmodel.cpp:214 in c6b635fb61 outdated
    209@@ -210,7 +210,8 @@ bool ClientModel::isReleaseVersion() const
    210 
    211 QString ClientModel::formatClientStartupTime() const
    212 {
    213-    return QDateTime::fromSecsSinceEpoch(GetStartupTime()).toString();
    214+    // Derive startup time from wall clock time and monotonic uptime.
    215+    return QDateTime::fromSecsSinceEpoch(GetTime() - GetUptime()).toString();
    216 }
    


    maflcko commented at 8:27 am on January 19, 2026:
    Not sure if this is accurate. I think it would be better to change the gui to also use raw monotonic uptime, instead of made-up wall clock timestamps.

    l0rinc commented at 10:19 am on January 19, 2026:
    Isn’t this what users would be interested in, what they would see on their clocks locally? @achow101, can you help us out here?

    maflcko commented at 10:37 am on January 19, 2026:
    Yeah, I guess it is just a nit. Would seem a bit odd to have the gui startup time not refer to any startup time written to the debug log, but I guess all odds are lost when the user is using the wrong clock.

    l0rinc commented at 11:43 am on January 19, 2026:
    Yeah, it’s the least awkward solution I found
  5. in src/common/system.h:16 in c6b635fb61
    11@@ -12,8 +12,8 @@
    12 #include <optional>
    13 #include <string>
    14 
    15-// Application startup time (used for uptime calculation)
    16-int64_t GetStartupTime();
    17+// Monotonic uptime in seconds (not affected by system time changes).
    18+int64_t GetUptime();
    


    maflcko commented at 10:35 am on January 19, 2026:
    nit: You are changing all call-sites anyway, so my preference would be to return the full SteadyClock resolution here, and only cast at call-sites, when needed.

    l0rinc commented at 11:42 am on January 19, 2026:
    I can add a TicksSeconds helper which would keep the result small and focused and only collapse the type as late as possible - taken, thanks
  6. in src/common/system.cpp:41 in c6b635fb61
    36@@ -37,8 +37,9 @@
    37 
    38 using util::ReplaceAll;
    39 
    40-// Application startup time (used for uptime calculation)
    41-const int64_t nStartupTime = GetTime();
    42+namespace {
    43+const SteadySeconds startup_time{Now<SteadySeconds>()};
    


    maflcko commented at 10:36 am on January 19, 2026:
    nit: Could inline this into the function body to reduce the scope? Also, globals usually start with g_

    l0rinc commented at 11:39 am on January 19, 2026:
    That would increase the diff, but simplify the result - taken, added you as coauthor, thanks
  7. in test/functional/rpc_uptime.py:37 in c6b635fb61
    35+        self.nodes[0].setmocktime(int(time.time()) + 20_000_000)  # Simulate an NTP correction.
    36+        uptime_after = self.nodes[0].uptime()
    37+        self.nodes[0].setmocktime(0)
    38+
    39+        after = time.monotonic()
    40+        assert uptime_after - uptime_before <= int(after - before), "uptime should not jump with wall clock"
    


    maflcko commented at 10:41 am on January 19, 2026:
    0        assert uptime_after - uptime_before <= wait_time, "uptime should not jump with wall clock"
    

    Why not just keep the previous variable and logic? Possibly with wait_time=20_000


    l0rinc commented at 11:41 am on January 19, 2026:
    The previous logic didn’t recreate the problem, but I can simplify to just state that the actual uptime reported is small (instead of comparing it to actual elapsed).
  8. maflcko approved
  9. maflcko commented at 10:43 am on January 19, 2026: member
    lgtm. Left some nits, but feel free to ignore them.
  10. util: add `TicksSeconds`
    Add a helper to convert durations to integer seconds.
    a9440b1595
  11. l0rinc force-pushed on Jan 19, 2026
  12. l0rinc commented at 11:57 am on January 19, 2026: contributor

    Thanks for the review and suggestions, kept the duration type, added a TickSeconds helper and simplified the test to check for rough elapsed time instead.

    Re-ran manual and automated test before/after, updated PR description, added you as coauthor.

  13. l0rinc force-pushed on Jan 19, 2026
  14. DrahtBot added the label CI failed on Jan 19, 2026
  15. in src/common/system.cpp:132 in 38b924fcf1 outdated
    126@@ -130,8 +127,8 @@ std::optional<size_t> GetTotalRAM()
    127     return std::nullopt;
    128 }
    129 
    130-// Obtain the application startup time (used for uptime calculation)
    131-int64_t GetStartupTime()
    132+SteadyClock::duration GetUptime()
    133 {
    134-    return nStartupTime;
    135+    static const auto startup_time{SteadyClock::now()};
    


    maflcko commented at 12:07 pm on January 19, 2026:
    nit: Still missing g_ prefix for this global?

    l0rinc commented at 1:22 pm on January 19, 2026:
    It’s function-local static with reduced scope, so it’s a file-scope global, I prefer keeping it as is.

    maflcko commented at 3:56 pm on January 19, 2026:

    Yeah, I think those are using the g_ prefix as well, see:

    0src/random.cpp:RNGState& GetRNGState() noexcept
    1src/random.cpp-{
    2src/random.cpp-    // This idiom relies on the guarantee that static variable are initialized
    3src/random.cpp-    // on first call, even when multiple parallel calls are permitted.
    4src/random.cpp-    static std::vector<RNGState, secure_allocator<RNGState>> g_rng(1);
    5src/random.cpp-    return g_rng[0];
    6src/random.cpp-}
    

    l0rinc commented at 4:11 pm on January 19, 2026:
    ok, done
  16. in src/common/system.h:17 in 38b924fcf1
    14 #include <optional>
    15 #include <string>
    16 
    17-// Application startup time (used for uptime calculation)
    18-int64_t GetStartupTime();
    19+// Monotonic uptime (not affected by system time changes).
    


    maflcko commented at 12:09 pm on January 19, 2026:
    0/// Monotonic uptime (not affected by system time changes).
    

    nit: I think doxygen comments require three ///.


    l0rinc commented at 1:36 pm on January 19, 2026:
    Sure, done
  17. in src/util/time.h:81 in 38b924fcf1 outdated
    73@@ -74,6 +74,12 @@ constexpr auto Ticks(Dur2 d)
    74 {
    75     return std::chrono::duration_cast<Dur1>(d).count();
    76 }
    77+
    78+template <typename Duration>
    79+constexpr int64_t TicksSeconds(Duration d)
    80+{
    81+    return int64_t{Ticks<std::chrono::seconds>(d)};
    


    maflcko commented at 12:11 pm on January 19, 2026:
    not sure about this alias. I think it is fine to just use the Ticks<...> helper directly. Otherwise, we’ll add an alias for every chrono duration type?

    l0rinc commented at 1:22 pm on January 19, 2026:
    We already have helpers for most of these, seconds seem common enough that I would like to cover it with a dedicated helper, the alternative is very noisy in my opinion

    maflcko commented at 4:02 pm on January 19, 2026:

    I think it is good for casts to be noisy, as they should only be used rarely (rpc interface, debug log, …). Also, it seems good to see at the call site that a cast is done via the angle-brackets.

    The count_* helpers below exist, because they are cast-free.

    But just a nit, not a blocker.

  18. in src/qt/clientmodel.cpp:214 in 38b924fcf1
    209@@ -210,7 +210,8 @@ bool ClientModel::isReleaseVersion() const
    210 
    211 QString ClientModel::formatClientStartupTime() const
    212 {
    213-    return QDateTime::fromSecsSinceEpoch(GetStartupTime()).toString();
    214+    // Derive startup time from wall clock time and monotonic uptime.
    215+    return QDateTime::fromSecsSinceEpoch(GetTime() - TicksSeconds(GetUptime())).toString();
    


    maflcko commented at 12:23 pm on January 19, 2026:

    nit: Seems fine, but not a fan of introducing more use of the deprecated GetTime. I think

    0    return QDateTime::currentDateTime().addSecs(Ticks<...>(GetUptime()).toString();
    

    l0rinc commented at 1:37 pm on January 19, 2026:
    Sure, done
  19. in test/functional/rpc_uptime.py:33 in 38b924fcf1
    31-        assert self.nodes[0].uptime() >= wait_time
    32+        uptime_before = self.nodes[0].uptime()
    33+        self.nodes[0].setmocktime(int(time.time()) + 20_000_000)
    34+        uptime_after = self.nodes[0].uptime()
    35+        self.nodes[0].setmocktime(0)
    36+        assert uptime_after - uptime_before < 60, "uptime should not jump with wall clock"
    


    maflcko commented at 12:25 pm on January 19, 2026:
    0    assert uptime_after - uptime_before < wait_time  # uptime should not jump with wall clock
    

    nit: Could use wait_time here, to avoid something hard-coded? (Here and in setmocktime)


    l0rinc commented at 1:37 pm on January 19, 2026:
    I’m not sure what wait_time refers to, added self.rpc_timeout since that should be a hard limit.

    maflcko commented at 3:56 pm on January 19, 2026:

    I’m not sure what wait_time refers to, added self.rpc_timeout since that should be a hard limit.

    It is just the name of the symbol that refers to a duration used in this test:

    0wait_time = 20_000
    1uptime_before = self.nodes[0].uptime()
    2self.nodes[0].setmocktime(int(time.time() + wait_time))
    3uptime_after = self.nodes[0].uptime()
    4assert uptime_after - uptime_before < wait_time  # uptime should not jump with wall clock
    5self.nodes[0].setmocktime(0)
    

    I think the above test should fail on current master and pass with this pull request.


    l0rinc commented at 4:01 pm on January 19, 2026:
    Do you have anything against the current solution?

    l0rinc commented at 4:10 pm on January 19, 2026:

    I don’t have strong preference, but changed it anyway.

    I think the above test should fail on current master

    yes:

    0  File "/Users/lorinc/IdeaProjects/bitcoin/build/test/functional/rpc_uptime.py", line 23, in run_test
    1    self._test_uptime()
    2    ~~~~~~~~~~~~~~~~~^^
    3  File "/Users/lorinc/IdeaProjects/bitcoin/build/test/functional/rpc_uptime.py", line 34, in _test_uptime
    4    assert uptime_after - uptime_before < wait_time, "uptime should not jump with wall clock"
    5           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    6AssertionError: uptime should not jump with wall clock
    
  20. maflcko approved
  21. maflcko commented at 12:27 pm on January 19, 2026: member
    lgtm, just some more nits 😅
  22. l0rinc force-pushed on Jan 19, 2026
  23. DrahtBot removed the label CI failed on Jan 19, 2026
  24. rpc: make `uptime` monotonic across NTP jumps
    Compute `uptime` from `SteadyClock` so it is unaffected by system time changes after startup.
    
    Derive GUI startup time by subtracting the monotonic uptime from the wall clock time.
    
    Add a functional test covering a large `setmocktime` jump.
    
    Co-authored-by: MarcoFalke <*~=`'#}+{/-|&$^_@721217.xyz>
    14f99cfe53
  25. l0rinc force-pushed on Jan 19, 2026
  26. in test/functional/rpc_uptime.py:31 in 14f99cfe53
    25@@ -26,9 +26,12 @@ def _test_negative_time(self):
    26         assert_raises_rpc_error(-8, "Mocktime must be in the range [0, 9223372036], not -1.", self.nodes[0].setmocktime, -1)
    27 
    28     def _test_uptime(self):
    29-        wait_time = 10
    30-        self.nodes[0].setmocktime(int(time.time() + wait_time))
    31-        assert self.nodes[0].uptime() >= wait_time
    32+        wait_time = 20_000
    33+        uptime_before = self.nodes[0].uptime()
    34+        self.nodes[0].setmocktime(int(time.time()) + wait_time)
    


    maflcko commented at 4:27 pm on January 19, 2026:

    style-nit, to make the diff smaller:

    0self.nodes[0].setmocktime(int(time.time() + wait_time))
    

    l0rinc commented at 4:36 pm on January 19, 2026:
    I did it deliberately to avoid + overload - seemed simpler
  27. in test/functional/rpc_uptime.py:34 in 14f99cfe53
    32+        wait_time = 20_000
    33+        uptime_before = self.nodes[0].uptime()
    34+        self.nodes[0].setmocktime(int(time.time()) + wait_time)
    35+        uptime_after = self.nodes[0].uptime()
    36+        self.nodes[0].setmocktime(0)
    37+        assert uptime_after - uptime_before < wait_time, "uptime should not jump with wall clock"
    


    maflcko commented at 4:28 pm on January 19, 2026:

    llm-nit: Possible places where comparison-specific test macros should replace generic comparisons:

    • test/functional/rpc_uptime.py “assert uptime_after - uptime_before < wait_time, “uptime should not jump with wall clock”” -> recommendation: use the comparison helper by reversing operands, e.g. assert_greater_than(wait_time, uptime_after - uptime_before), to avoid a bare assert.

    l0rinc commented at 3:03 pm on January 20, 2026:
    I find this unreadable, I will use them when someone renames it to assert_gt or something similar that’s less noisy.

    maflcko commented at 1:49 pm on January 21, 2026:

    The utility exits so that intermittent failures are easier to debug. Arguably not an issue here, but I think if a utility was added for a purpose, it should be used for that purpose consistently.

    If you don’t like the name, it can be renamed. I don’t care about the name, so I am happy to ack. I mostly care about the functionality.


    willcl-ark commented at 3:05 pm on January 23, 2026:

    Would be nice to be able to use the helper but also get the string context 🤷🏼‍♂️

    I don’t see any difference between assert_gt and assert_greater_than myself though…


    l0rinc commented at 4:10 pm on January 23, 2026:

    Boost has BOOST_CHECK_GE while the python equivalent is assert_greater_than_or_equal - that’s a whole line of noise replacing >= - I’ll push a PR to unify them with the boost checks to improve the signal-to-noise ratio a bit…

    Edit: pushed to https://github.com/bitcoin/bitcoin/pull/34395

  28. maflcko approved
  29. maflcko commented at 4:28 pm on January 19, 2026: member

    looks like the llm printed another nit (feel free to ignore)

    review ACK 14f99cfe53f07280b6f047844fc4fba0da8cd328 🎦

    Signature:

    0untrusted comment: signature from minisign secret key on empty file; verify via: minisign -Vm "${path_to_any_empty_file}" -P RWTRmVTMeKV5noAMqVlsMugDDCyyTSbA3Re5AkUrhvLVln0tSaFWglOw -x "${path_to_this_whole_four_line_signature_blob}"
    1RUTRmVTMeKV5npGrKx1nqXCw5zeVHdtdYURB/KlyA/LMFgpNCs+SkW9a8N95d+U4AP1RJMi+krxU1A3Yux4bpwZNLvVBKy0wLgM=
    2trusted comment: review ACK 14f99cfe53f07280b6f047844fc4fba0da8cd328 🎦
    3fgHS4+gshJQWq7jBqXAoae4ypq7MhlrlHzwQfA89Ki1dofJBqqYDv60oTyBpwj3wanaVllKaQtDl2JPfDx1OAw==
    
  30. in src/util/time.h:82 in a9440b1595 outdated
    73@@ -74,6 +74,12 @@ constexpr auto Ticks(Dur2 d)
    74 {
    75     return std::chrono::duration_cast<Dur1>(d).count();
    76 }
    77+
    78+template <typename Duration>
    79+constexpr int64_t TicksSeconds(Duration d)
    80+{
    81+    return int64_t{Ticks<std::chrono::seconds>(d)};
    82+}
    


    willcl-ark commented at 2:59 pm on January 23, 2026:
    nit: If you touch again could add an empty line here :D

    l0rinc commented at 4:04 pm on January 23, 2026:
    It was deliberate, but I don’t mind doing it if I have to retouch
  31. willcl-ark approved
  32. willcl-ark commented at 3:06 pm on January 23, 2026: member

    tACK 14f99cfe53f07280b6f047844fc4fba0da8cd328

    Tested to make sure it works.

    Implementation is good for me. Left a nit in case you touch again.

  33. w0xlt commented at 2:55 am on January 24, 2026: contributor

    ACK 14f99cfe53f07280b6f047844fc4fba0da8cd328

    nit: TicksSeconds may be redundant. The Ticks<std::chrono::seconds> pattern is already well-established. Adding TicksSeconds creates a precedent for adding TicksMilliseconds, TicksMicroseconds, etc.

  34. carloantinarella commented at 8:33 pm on January 25, 2026: none

    Manually tested 14f99cf on Linux with -regtest. Steps:

    1. bitcoind -regtest
    2. Wait 10s
    3. bitcoin-cli -regtest uptime

    Result: 0 is shown. In general, on the first invocation, the command consistently returned 0. Subsequent calls increased monotonically as expected.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-01-27 06:13 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me