type-chrono: Add CustomBuildField/CustomReadField overloads for std::chrono::time_point#303
type-chrono: Add CustomBuildField/CustomReadField overloads for std::chrono::time_point#303ryanofsky wants to merge 1 commit into
Conversation
…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>
|
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. ReviewsSee the guideline and AI policy for information on the review process.
If your review is incorrectly listed, please copy-paste |
|
lgtm ACK b37d1d7 |
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. |
|
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 |
| Value&& value, Output&& output) | ||
| { | ||
| using Rep = typename Duration::rep; | ||
| static_assert(std::numeric_limits<decltype(output.get())>::lowest() <= std::numeric_limits<Rep>::lowest(), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Needed for bitcoin/bitcoin#10102 since bitcoin/bitcoin#34882 was merged, which uses
NodeClock::time_pointin theCNodeStatsstruct (m_last_send,m_last_recv,m_ping_start) returned byNode::getNodesStats.