From 498a1ae3601b6e6b6990316c87d3fac13dbe3779 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 12 Aug 2026 15:37:50 -0500 Subject: [PATCH] fix(util): handle the UINT64_MAX-containing range in CRangesSet Adding UINT64_MAX to a CRangesSet stores the half-open range with a wrapped end of 0. Contains() then reports the value absent, so a duplicate Add() reaches the set-insert assert instead of returning false, and the asset-unlock duplicate-index check (evo/assetlocktx.cpp) would crash in GetCreditPool rather than cleanly rejecting the transaction with bad-assetunlock-duplicated-index. Size() computed the width of such a range through an unsigned wrap that -fsanitize=integer reports. Treat end == 0 as extends-through-UINT64_MAX in Contains() and Size(), and pin the boundary behavior (membership, sizing, duplicate detection, removal, re-add) in test_CRanges. The new checks fail eight ways on the previous implementation. Co-Authored-By: Claude Fable 5 --- src/test/util_tests.cpp | 29 +++++++++++++++++++++++++++++ src/util/ranges_set.cpp | 8 ++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp index d941032c97b9..2939888582ac 100644 --- a/src/test/util_tests.cpp +++ b/src/test/util_tests.cpp @@ -1423,6 +1423,35 @@ BOOST_AUTO_TEST_CASE(test_CRanges) BOOST_CHECK(ranges.Size() > ((1u << test) / 4)); } } + + // The range containing UINT64_MAX is stored with a wrapped half-open end + // of 0. Membership, sizing, duplicate detection, and removal must all + // treat that representation as "extends through the maximum value". + const uint64_t max{std::numeric_limits::max()}; + CRangesSet max_values; + BOOST_CHECK(max_values.Add(max - 2)); + BOOST_CHECK(max_values.Add(max - 1)); + BOOST_CHECK(max_values.Add(max)); + BOOST_CHECK_EQUAL(max_values.Size(), 3U); + BOOST_CHECK(max_values.Contains(max - 2)); + BOOST_CHECK(max_values.Contains(max - 1)); + BOOST_CHECK(max_values.Contains(max)); + BOOST_CHECK(!max_values.Contains(0)); + BOOST_CHECK(!max_values.Add(max)); + BOOST_CHECK(max_values.Remove(max)); + BOOST_CHECK_EQUAL(max_values.Size(), 2U); + BOOST_CHECK(max_values.Contains(max - 1)); + BOOST_CHECK(!max_values.Contains(max)); + BOOST_CHECK(max_values.Add(max)); + BOOST_CHECK(max_values.Contains(max)); + + CRangesSet lone_max; + BOOST_CHECK(lone_max.Add(max)); + BOOST_CHECK_EQUAL(lone_max.Size(), 1U); + BOOST_CHECK(lone_max.Contains(max)); + BOOST_CHECK(!lone_max.Add(max)); + BOOST_CHECK(lone_max.Remove(max)); + BOOST_CHECK(lone_max.IsEmpty()); } static std::string SpanToStr(const Span& span) diff --git a/src/util/ranges_set.cpp b/src/util/ranges_set.cpp index 11b7863a17f8..9e55dbfd8448 100644 --- a/src/util/ranges_set.cpp +++ b/src/util/ranges_set.cpp @@ -4,6 +4,8 @@ #include +#include + CRangesSet::Range::Range() : CRangesSet::Range::Range(0, 0) {} CRangesSet::Range::Range(uint64_t begin_in, uint64_t end_in) : @@ -81,7 +83,9 @@ size_t CRangesSet::Size() const noexcept { size_t result{0}; for (auto i : ranges) { - result += i.end - i.begin; + // end == 0 is the half-open representation of a range containing + // UINT64_MAX. Avoid the unsigned subtraction wrap for that range. + result += i.end == 0 ? std::numeric_limits::max() - i.begin + 1 : i.end - i.begin; } return result; } @@ -93,5 +97,5 @@ bool CRangesSet::Contains(uint64_t value) const noexcept if (it == ranges.begin()) return false; auto prev = it; --prev; - return prev->begin <= value && prev->end > value; + return prev->begin <= value && (prev->end == 0 || prev->end > value); }