type-chrono: Add CustomBuildField/CustomReadField overloads for std::chrono::time_point #303

pull ryanofsky wants to merge 3 commits into bitcoin-core:master from ryanofsky:pr/timepoint changing 3 files +53 −6
  1. ryanofsky commented at 6:08 PM on July 8, 2026: collaborator

    Needed for bitcoin/bitcoin#10102 since bitcoin/bitcoin#34882 was merged, which uses NodeClock::time_point in the CNodeStats struct (m_last_send, m_last_recv, m_ping_start) returned by Node::getNodesStats.

  2. DrahtBot commented at 6:08 PM on July 8, 2026: none

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    ACK maflcko, ViniciusCestarii

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  3. maflcko commented at 8:09 PM on July 8, 2026: contributor

    lgtm ACK b37d1d71f115f6b5a0000e09c7be063994387e1a

  4. Sjors commented at 11:55 AM on July 9, 2026: member

    Needed by https://github.com/bitcoin/bitcoin/pull/34882

    That's merged, by I assume you still need this?

  5. maflcko commented at 12:01 PM on July 9, 2026: contributor

    Needed by bitcoin/bitcoin#34882

    That's merged, by I assume you still need this?

    There was no rebase of https://github.com/bitcoin/bitcoin/pull/10102 in 4 months, so I don't think https://github.com/bitcoin/bitcoin/pull/10102 sees the changes from 34882 yet.

  6. ryanofsky commented at 12:42 PM on July 9, 2026: collaborator

    Sorry PR description was written in a confusing way, should hopefully be clearer now. In order for bitcoin/bitcoin#10102 to work after bitcoin/bitcoin#34882 it needs these ReadField / BuildField overloads. It could define them itself, but it makes sense for libmultiprocess to provide them since it already provides many similar ones including ones for std::chrono::duration

  7. in include/mp/type-chrono.h:42 in b37d1d71f1
      37 | +template <class Clock, class Duration, typename Value, typename Output>
      38 | +void CustomBuildField(TypeList<std::chrono::time_point<Clock, Duration>>, Priority<1>, InvokeContext& invoke_context,
      39 | +                      Value&& value, Output&& output)
      40 | +{
      41 | +    using Rep = typename Duration::rep;
      42 | +    static_assert(std::numeric_limits<decltype(output.get())>::lowest() <= std::numeric_limits<Rep>::lowest(),
    


    ViniciusCestarii commented at 1:17 PM on July 9, 2026:

    The range checks in both CustomBuildField overloads have a signedness bug:

    static_assert(std::numeric_limits<decltype(output.get())>::lowest() <= std::numeric_limits<Rep>::lowest(), ...);
    

    When the capnp field type is unsigned (e.g. UInt64) and Rep is signed (e.g. int64_t for std::chrono::nanoseconds), the usual arithmetic conversions convert Rep::lowest() (INT64_MIN) to the unsigned type before comparing, turning it into a huge positive number. So 0 <= INT64_MIN silently evaluates to true, and the assert passes for a field that can't actually represent negative tick counts (e.g. any pre-epoch system_clock::time_point).

    Concretely, this means a UInt64 field paired with a signed Duration::rep compiles today, and output.set(negative_count) wraps the value into UINT64_MAX. That's silently wrong for anything that reads the field as its declared (unsigned) type — a different language's capnp bindings, a JSON dump, or a raw comparison would see 18446744073709551615 instead of -1. A C++ round trip through the same chrono type can look fine, since the wrap is bit-for-bit invertible at equal width, which is what makes this easy to miss in testing.

    Fix: use std::cmp_less_equal/std::cmp_greater_equal (C++20, <utility>) instead of <=/>=, since they're designed to compare integers of differing signedness correctly:

    static_assert(std::cmp_less_equal(std::numeric_limits<decltype(output.get())>::lowest(), std::numeric_limits<Rep>::lowest()),
                  "capnp type does not have enough range to hold lowest std::chrono::time_point value");
    static_assert(std::cmp_greater_equal(std::numeric_limits<decltype(output.get())>::max(), std::numeric_limits<Rep>::max()),
                  "capnp type does not have enough range to hold highest std::chrono::time_point value");
    

    I verified this by temporarily declaring a test field as UInt64 against a signed Rep: it compiled before the fix and correctly fails to compile after.

    Happy to push chrono type tests as a follow-up PR if useful.

    Note: I had AI help me write this up clearly, apologies if the phrasing feels more polished or sloppy than my usual comments.


    ryanofsky commented at 2:09 PM on July 9, 2026:

    re: #303 (review)

    Good catch! I think this makes sense and it looks like this same problem also exists other places: for durations above and also in type-number.h. I'll see if it's possible to fix them all here without breaking anything.


    ryanofsky commented at 12:45 AM on July 30, 2026:

    re: #303 (review)

    Added a new commit to fix this problem. Using std::cmp functions directly didn't work for time_point comparisons because floating point time point types are used in some places so I had to add wrappers to handle floats.

  8. ryanofsky referenced this in commit 7dce1a915e on Jul 30, 2026
  9. ryanofsky force-pushed on Jul 30, 2026
  10. ryanofsky commented at 12:47 AM on July 30, 2026: collaborator

    Thanks for the reviews!

    <!-- begin push-2 -->

    Updated b37d1d71f115f6b5a0000e09c7be063994387e1a -> 978028ffd0d14e5e46b313412ef17b7ad3647e33 (pr/timepoint.1 -> pr/timepoint.2, compare)<!-- end --> switching to std::cmp functions for more reliable static asserts

  11. maflcko commented at 5:42 AM on July 30, 2026: contributor

    Looks like iwyu fails?

  12. ryanofsky referenced this in commit 2a4ef6318f on Jul 30, 2026
  13. ryanofsky force-pushed on Jul 30, 2026
  14. ryanofsky commented at 2:20 PM on July 30, 2026: collaborator

    <!-- begin push-3 -->

    Updated 978028ffd0d14e5e46b313412ef17b7ad3647e33 -> 1e3311158138ac9d458656805e666663f8f19f40 (pr/timepoint.2 -> pr/timepoint.3, compare)<!-- end --> to fix IWYU error https://github.com/bitcoin-core/libmultiprocess/actions/runs/30503532739/job/90748227074

    <!-- begin push-4 -->

    Rebased 1e3311158138ac9d458656805e666663f8f19f40 -> 7a72df02e2d46f953c4c97662c3e109aa3874ea9 (pr/timepoint.3 -> pr/timepoint.4, compare)<!-- end --> adding new commit and rebasing to fix incompatibility with bool and std::cmp_less_equal/std::cmp_greater_equal in libc++ https://github.com/bitcoin-core/libmultiprocess/actions/runs/30551079943/job/90899596238?pr=303

  15. type-number: exclude bool from the integral overload
    Exclude bool from the integral BuildPrimitive overload (bool is integral
    per std::is_integral_v but not a standard integer per std::cmp_*); add a
    dedicated bool overload that static_asserts the capnp LocalType is also
    bool, so a schema mismatch (e.g. Float64 used for a bool parameter)
    produces a clear error pointing at the schema.
    
    This change is also needed so the next commit can tighten the integer
    range changes in the BuildPrimitive integer overload.
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    45f6255975
  16. type-number, type-chrono: Fix static assert signed/unsigned comparisons
    Use std::cmp_less_equal/std::cmp_greater_equal (C++20) in the range
    static_asserts to avoid signed/unsigned comparison pitfall: when one side
    is unsigned and the other signed, the usual arithmetic conversions silently
    convert the signed lowest() to a huge positive number, making the assert
    pass when it should fire. Also fix the identical pattern in
    type-number.h BuildPrimitive.
    
    Problem was reported by ViniciusCestarii in
    https://github.com/bitcoin-core/libmultiprocess/pull/303#discussion_r3551873101
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    45b685c3f5
  17. type-chrono: Add CustomBuildField/CustomReadField overloads for std::chrono::time_point
    Needed by bitcoin/bitcoin#34882 which uses NodeClock::time_point in the
    Node stats struct (m_last_send, m_last_recv, m_ping_start), requiring
    IPC serialization support for time_point types.
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    7a72df02e2
  18. ryanofsky force-pushed on Jul 30, 2026
  19. in include/mp/util.h:48 in 7a72df02e2
      43 | +}
      44 | +template <typename A, typename B>
      45 | +constexpr bool safe_greater_equal(A a, B b)
      46 | +{
      47 | +    if constexpr (std::is_floating_point_v<A> || std::is_floating_point_v<B>) return a >= b;
      48 | +    else return std::cmp_greater_equal(a, b);
    


    uqlidi commented at 6:48 AM on July 31, 2026:
        else return safe_less_equal(b, a);
    

    ryanofsky commented at 1:37 PM on August 3, 2026:

    re: #303 (review)

    This does seem like a good idea, but it's a minor cleanup so not planning to make the change here. Could be worth doing if this code changes again.

  20. uqlidi changes_requested
  21. uqlidi commented at 6:49 AM on July 31, 2026: none

    looks good but there's a small suggestion

  22. maflcko commented at 8:41 AM on July 31, 2026: contributor

    lgtm ACK 7a72df02e2d46f953c4c97662c3e109aa3874ea9

  23. ViniciusCestarii commented at 6:11 PM on August 1, 2026: contributor

    ACK 7a72df02e2d46f953c4c97662c3e109aa3874ea9

  24. ryanofsky merged this on Aug 3, 2026
  25. ryanofsky closed this on Aug 3, 2026


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin-core/libmultiprocess. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-08-05 20:30 UTC

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