From b2e0f666a2109235b2da5c7ce75495f966a86a78 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Mon, 29 Jul 2024 16:06:30 -0400 Subject: [PATCH] feefrac: add support for evaluating at given size --- src/test/feefrac_tests.cpp | 40 +++++++++++++++++++++++++++++++++++++- src/test/fuzz/feefrac.cpp | 13 ++++++++++--- src/util/feefrac.h | 21 ++++++++++++++++++++ 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/test/feefrac_tests.cpp b/src/test/feefrac_tests.cpp index 41c9c0a6337..03addeeff59 100644 --- a/src/test/feefrac_tests.cpp +++ b/src/test/feefrac_tests.cpp @@ -15,7 +15,28 @@ BOOST_AUTO_TEST_CASE(feefrac_operators) FeeFrac sum{1500, 400}; FeeFrac diff{500, -200}; FeeFrac empty{0, 0}; - [[maybe_unused]] FeeFrac zero_fee{0, 1}; // zero-fee allowed + FeeFrac zero_fee{0, 1}; // zero-fee allowed + + BOOST_CHECK_EQUAL(zero_fee.EvaluateFee(0), 0); + BOOST_CHECK_EQUAL(zero_fee.EvaluateFee(1), 0); + BOOST_CHECK_EQUAL(zero_fee.EvaluateFee(1000000), 0); + BOOST_CHECK_EQUAL(zero_fee.EvaluateFee(0x7fffffff), 0); + + BOOST_CHECK_EQUAL(p1.EvaluateFee(0), 0); + BOOST_CHECK_EQUAL(p1.EvaluateFee(1), 10); + BOOST_CHECK_EQUAL(p1.EvaluateFee(100000000), 1000000000); + BOOST_CHECK_EQUAL(p1.EvaluateFee(0x7fffffff), int64_t(0x7fffffff) * 10); + + FeeFrac neg{-1001, 100}; + BOOST_CHECK_EQUAL(neg.EvaluateFee(0), 0); + BOOST_CHECK_EQUAL(neg.EvaluateFee(1), -11); + BOOST_CHECK_EQUAL(neg.EvaluateFee(2), -21); + BOOST_CHECK_EQUAL(neg.EvaluateFee(3), -31); + BOOST_CHECK_EQUAL(neg.EvaluateFee(100), -1001); + BOOST_CHECK_EQUAL(neg.EvaluateFee(101), -1012); + BOOST_CHECK_EQUAL(neg.EvaluateFee(100000000), -1001000000); + BOOST_CHECK_EQUAL(neg.EvaluateFee(100000001), -1001000011); + BOOST_CHECK_EQUAL(neg.EvaluateFee(0x7fffffff), -21496311307); BOOST_CHECK(empty == FeeFrac{}); // same as no-args @@ -67,6 +88,16 @@ BOOST_AUTO_TEST_CASE(feefrac_operators) BOOST_CHECK(oversized_1 << oversized_2); BOOST_CHECK(oversized_1 != oversized_2); + BOOST_CHECK_EQUAL(oversized_1.EvaluateFee(0), 0); + BOOST_CHECK_EQUAL(oversized_1.EvaluateFee(1), 1152921); + BOOST_CHECK_EQUAL(oversized_1.EvaluateFee(2), 2305843); + BOOST_CHECK_EQUAL(oversized_1.EvaluateFee(1548031267), 1784758530396540); + + // Test cases on the threshold where FeeFrac::EvaluateFee start using Mul/Div. + BOOST_CHECK_EQUAL(FeeFrac(0x1ffffffff, 123456789).EvaluateFee(98765432), 6871947728); + BOOST_CHECK_EQUAL(FeeFrac(0x200000000, 123456789).EvaluateFee(98765432), 6871947729); + BOOST_CHECK_EQUAL(FeeFrac(0x200000001, 123456789).EvaluateFee(98765432), 6871947730); + // Tests paths that use double arithmetic FeeFrac busted{(static_cast(INT32_MAX)) + 1, INT32_MAX}; BOOST_CHECK(!(busted < busted)); @@ -77,6 +108,13 @@ BOOST_AUTO_TEST_CASE(feefrac_operators) BOOST_CHECK(max_fee <= max_fee); BOOST_CHECK(max_fee >= max_fee); + BOOST_CHECK_EQUAL(max_fee.EvaluateFee(0), 0); + BOOST_CHECK_EQUAL(max_fee.EvaluateFee(1), 977888); + BOOST_CHECK_EQUAL(max_fee.EvaluateFee(2), 1955777); + BOOST_CHECK_EQUAL(max_fee.EvaluateFee(3), 2933666); + BOOST_CHECK_EQUAL(max_fee.EvaluateFee(1256796054), 1229006664189047); + BOOST_CHECK_EQUAL(max_fee.EvaluateFee(INT32_MAX), 2100000000000000); + FeeFrac max_fee2{1, 1}; BOOST_CHECK(max_fee >= max_fee2); diff --git a/src/test/fuzz/feefrac.cpp b/src/test/fuzz/feefrac.cpp index 28ecfb14a55..5b94bd232cd 100644 --- a/src/test/fuzz/feefrac.cpp +++ b/src/test/fuzz/feefrac.cpp @@ -136,7 +136,7 @@ FUZZ_TARGET(feefrac_div_fallback) assert(Abs256(res) == quot_abs); // Compare approximately with floating-point. - long double expect = std::floorl(num_high * 4294967296.0L + num_low) / den; + long double expect = std::floor(num_high * 4294967296.0L + num_low) / den; // Expect to be accurate within 50 bits of precision, +- 1 sat. if (expect == 0.0L) { assert(res >= -1 && res <= 1); @@ -173,7 +173,8 @@ FUZZ_TARGET(feefrac_mul_div) // If the result is not representable by an int64_t, bail out. if ((is_negative && quot_abs > MAX_ABS_INT64) || (!is_negative && quot_abs >= MAX_ABS_INT64)) { - // If 0 <= mul32 <= div, then the result is guaranteed to be representable. + // If 0 <= mul32 <= div, then the result is guaranteed to be representable. In the context + // of the Evaluate call below, this corresponds to 0 <= at_size <= feefrac.size. assert(mul32 < 0 || mul32 > div); return; } @@ -188,7 +189,7 @@ FUZZ_TARGET(feefrac_mul_div) assert(res == res_fallback); // Compare approximately with floating-point. - long double expect = std::floorl(static_cast(mul32) * mul64 / div); + long double expect = std::floor(static_cast(mul32) * mul64 / div); // Expect to be accurate within 50 bits of precision, +- 1 sat. if (expect == 0.0L) { assert(res >= -1 && res <= 1); @@ -199,4 +200,10 @@ FUZZ_TARGET(feefrac_mul_div) assert(res >= expect * 1.000000000000001L - 1.0L); assert(res <= expect * 0.999999999999999L + 1.0L); } + + // Verify the behavior of FeeFrac::Evaluate. + if (mul32 >= 0) { + auto res_fee = FeeFrac{mul64, div}.EvaluateFee(mul32); + assert(res == res_fee); + } } diff --git a/src/util/feefrac.h b/src/util/feefrac.h index 7c2e965e922..7cee46e79e5 100644 --- a/src/util/feefrac.h +++ b/src/util/feefrac.h @@ -182,6 +182,27 @@ struct FeeFrac std::swap(a.fee, b.fee); std::swap(a.size, b.size); } + + /** Compute the fee for a given size `at_size` using this object's feerate. + * + * This effectively corresponds to evaluating (this->fee * at_size) / this->size, with the + * result rounded down (even for negative feerates). + * + * Requires this->size > 0, at_size >= 0, and that the correct result fits in a int64_t. This + * is guaranteed to be the case when 0 <= at_size <= this->size. + */ + int64_t EvaluateFee(int32_t at_size) const noexcept + { + Assume(size > 0); + Assume(at_size >= 0); + if (fee >= 0 && fee < 0x200000000) [[likely]] { + // Common case where (this->fee * at_size) is guaranteed to fit in a uint64_t. + return (uint64_t(fee) * at_size) / uint32_t(size); + } else { + // Otherwise, use Mul and Div. + return Div(Mul(fee, at_size), size); + } + } }; /** Compare the feerate diagrams implied by the provided sorted chunks data.