Skip to content

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

Merged
ryanofsky merged 3 commits into
bitcoin-core:masterfrom
ryanofsky:pr/timepoint
Aug 3, 2026
Merged

type-chrono: Add CustomBuildField/CustomReadField overloads for std::chrono::time_point#303
ryanofsky merged 3 commits into
bitcoin-core:masterfrom
ryanofsky:pr/timepoint

Conversation

@ryanofsky

@ryanofsky ryanofsky commented Jul 8, 2026

Copy link
Copy Markdown
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.

@DrahtBot

DrahtBot commented Jul 8, 2026

Copy link
Copy Markdown

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

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 <!--meta-tag:bot-skip--> into the comment that the bot should ignore.

@maflcko

maflcko commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

lgtm ACK b37d1d7

@Sjors

Sjors commented Jul 9, 2026

Copy link
Copy Markdown
Member

Needed by bitcoin/bitcoin#34882

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

@maflcko

maflcko commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Needed by bitcoin/bitcoin#34882

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

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

@ryanofsky

Copy link
Copy Markdown
Collaborator Author

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

Comment thread include/mp/type-chrono.h Outdated
Value&& value, Output&& output)
{
using Rep = typename Duration::rep;
static_assert(std::numeric_limits<decltype(output.get())>::lowest() <= std::numeric_limits<Rep>::lowest(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, ) 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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #303 (comment)

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #303 (comment)

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.

ryanofsky added a commit to ryanofsky/libmultiprocess that referenced this pull request Jul 30, 2026
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
bitcoin-core#303 (comment)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@ryanofsky ryanofsky left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the reviews!

Updated b37d1d7 -> 978028f (pr/timepoint.1 -> pr/timepoint.2, compare) switching to std::cmp functions for more reliable static asserts

Comment thread include/mp/type-chrono.h Outdated
Value&& value, Output&& output)
{
using Rep = typename Duration::rep;
static_assert(std::numeric_limits<decltype(output.get())>::lowest() <= std::numeric_limits<Rep>::lowest(),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #303 (comment)

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.

@maflcko

maflcko commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Looks like iwyu fails?

ryanofsky added a commit to ryanofsky/libmultiprocess that referenced this pull request Jul 30, 2026
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
bitcoin-core#303 (comment)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@ryanofsky ryanofsky left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ryanofsky and others added 3 commits July 30, 2026 11:19
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>
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
bitcoin-core#303 (comment)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…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>

@uqlidi uqlidi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good but there's a small suggestion

Comment thread include/mp/util.h
Comment on lines +47 to +48
if constexpr (std::is_floating_point_v<A> || std::is_floating_point_v<B>) return a >= b;
else return std::cmp_greater_equal(a, b);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if constexpr (std::is_floating_point_v<A> || std::is_floating_point_v<B>) return a >= b;
else return std::cmp_greater_equal(a, b);
else return safe_less_equal(b, a);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #303 (comment)

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.

@maflcko

maflcko commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

lgtm ACK 7a72df0

@ViniciusCestarii ViniciusCestarii left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 7a72df0

@ryanofsky
ryanofsky merged commit f355108 into bitcoin-core:master Aug 3, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants