Skip to content

Commit cfe025f

Browse files
committed
Merge bitcoin/bitcoin#30535: feefrac: add support for evaluating at given size
58914ab fuzz: assert min diff between FeeFrac and CFeeRate (Pieter Wuille) 0c6bcfd feefrac: support both rounding up and down for Evaluate (Pieter Wuille) ecf956e feefrac: add support for evaluating at given size (Pieter Wuille) 7963aec feefrac: add helper functions for 96-bit division (Pieter Wuille) 800c0de feefrac: rework comments around Mul/MulFallback (Pieter Wuille) fcfe008 feefrac fuzz: use arith_uint256 instead of ad-hoc multiply (Pieter Wuille) 46ff422 arith_uint256: modernize comparison operators (Pieter Wuille) Pull request description: The `FeeFrac` type represents a fraction, intended to be used for sats/vbyte or sats/WU. This PR adds functionality to evaluate that feerate for a given size, in order to obtain the fee it corresponds with (rounding down, or rounding up). The motivation here is being able to do accurate feerate evaluations in cluster mempool block building heuristics (where rounding down is needed), but in principle this makes it possible to use `FeeFrac` as a more accurate replacement for `CFeeRate` (where for feerate estimation rounding up is desirable). Because of this, both rounding modes are implemented. Unit tests are included for known-correct values, plus a fuzz test that verifies the result using `arith_uint256`. ACKs for top commit: l0rinc: ACK 58914ab ismaelsadeeq: reACK 58914ab glozow: light code review ACK 58914ab Tree-SHA512: 362b88454bf355cae1f12d6430b1bb9ab66824140e12b27db7c48385f1e8db936da7d0694fb5aad2a00eb9e5fe3083a3a2c0cc40b2a68e2d37e07b3481d4eeae
2 parents 0dc74c9 + 58914ab commit cfe025f

File tree

4 files changed

+310
-60
lines changed

4 files changed

+310
-60
lines changed

src/arith_uint256.h

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#ifndef BITCOIN_ARITH_UINT256_H
77
#define BITCOIN_ARITH_UINT256_H
88

9+
#include <compare>
910
#include <cstdint>
1011
#include <cstring>
1112
#include <limits>
@@ -212,13 +213,8 @@ class base_uint
212213
friend inline base_uint operator<<(const base_uint& a, int shift) { return base_uint(a) <<= shift; }
213214
friend inline base_uint operator*(const base_uint& a, uint32_t b) { return base_uint(a) *= b; }
214215
friend inline bool operator==(const base_uint& a, const base_uint& b) { return memcmp(a.pn, b.pn, sizeof(a.pn)) == 0; }
215-
friend inline bool operator!=(const base_uint& a, const base_uint& b) { return memcmp(a.pn, b.pn, sizeof(a.pn)) != 0; }
216-
friend inline bool operator>(const base_uint& a, const base_uint& b) { return a.CompareTo(b) > 0; }
217-
friend inline bool operator<(const base_uint& a, const base_uint& b) { return a.CompareTo(b) < 0; }
218-
friend inline bool operator>=(const base_uint& a, const base_uint& b) { return a.CompareTo(b) >= 0; }
219-
friend inline bool operator<=(const base_uint& a, const base_uint& b) { return a.CompareTo(b) <= 0; }
216+
friend inline std::strong_ordering operator<=>(const base_uint& a, const base_uint& b) { return a.CompareTo(b) <=> 0; }
220217
friend inline bool operator==(const base_uint& a, uint64_t b) { return a.EqualTo(b); }
221-
friend inline bool operator!=(const base_uint& a, uint64_t b) { return !a.EqualTo(b); }
222218

223219
/** Hex encoding of the number (with the most significant digits first). */
224220
std::string GetHex() const;

src/test/feefrac_tests.cpp

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,45 @@ BOOST_AUTO_TEST_CASE(feefrac_operators)
1515
FeeFrac sum{1500, 400};
1616
FeeFrac diff{500, -200};
1717
FeeFrac empty{0, 0};
18-
[[maybe_unused]] FeeFrac zero_fee{0, 1}; // zero-fee allowed
18+
FeeFrac zero_fee{0, 1}; // zero-fee allowed
19+
20+
BOOST_CHECK_EQUAL(zero_fee.EvaluateFeeDown(0), 0);
21+
BOOST_CHECK_EQUAL(zero_fee.EvaluateFeeDown(1), 0);
22+
BOOST_CHECK_EQUAL(zero_fee.EvaluateFeeDown(1000000), 0);
23+
BOOST_CHECK_EQUAL(zero_fee.EvaluateFeeDown(0x7fffffff), 0);
24+
BOOST_CHECK_EQUAL(zero_fee.EvaluateFeeUp(0), 0);
25+
BOOST_CHECK_EQUAL(zero_fee.EvaluateFeeUp(1), 0);
26+
BOOST_CHECK_EQUAL(zero_fee.EvaluateFeeUp(1000000), 0);
27+
BOOST_CHECK_EQUAL(zero_fee.EvaluateFeeUp(0x7fffffff), 0);
28+
29+
BOOST_CHECK_EQUAL(p1.EvaluateFeeDown(0), 0);
30+
BOOST_CHECK_EQUAL(p1.EvaluateFeeDown(1), 10);
31+
BOOST_CHECK_EQUAL(p1.EvaluateFeeDown(100000000), 1000000000);
32+
BOOST_CHECK_EQUAL(p1.EvaluateFeeDown(0x7fffffff), int64_t(0x7fffffff) * 10);
33+
BOOST_CHECK_EQUAL(p1.EvaluateFeeUp(0), 0);
34+
BOOST_CHECK_EQUAL(p1.EvaluateFeeUp(1), 10);
35+
BOOST_CHECK_EQUAL(p1.EvaluateFeeUp(100000000), 1000000000);
36+
BOOST_CHECK_EQUAL(p1.EvaluateFeeUp(0x7fffffff), int64_t(0x7fffffff) * 10);
37+
38+
FeeFrac neg{-1001, 100};
39+
BOOST_CHECK_EQUAL(neg.EvaluateFeeDown(0), 0);
40+
BOOST_CHECK_EQUAL(neg.EvaluateFeeDown(1), -11);
41+
BOOST_CHECK_EQUAL(neg.EvaluateFeeDown(2), -21);
42+
BOOST_CHECK_EQUAL(neg.EvaluateFeeDown(3), -31);
43+
BOOST_CHECK_EQUAL(neg.EvaluateFeeDown(100), -1001);
44+
BOOST_CHECK_EQUAL(neg.EvaluateFeeDown(101), -1012);
45+
BOOST_CHECK_EQUAL(neg.EvaluateFeeDown(100000000), -1001000000);
46+
BOOST_CHECK_EQUAL(neg.EvaluateFeeDown(100000001), -1001000011);
47+
BOOST_CHECK_EQUAL(neg.EvaluateFeeDown(0x7fffffff), -21496311307);
48+
BOOST_CHECK_EQUAL(neg.EvaluateFeeUp(0), 0);
49+
BOOST_CHECK_EQUAL(neg.EvaluateFeeUp(1), -10);
50+
BOOST_CHECK_EQUAL(neg.EvaluateFeeUp(2), -20);
51+
BOOST_CHECK_EQUAL(neg.EvaluateFeeUp(3), -30);
52+
BOOST_CHECK_EQUAL(neg.EvaluateFeeUp(100), -1001);
53+
BOOST_CHECK_EQUAL(neg.EvaluateFeeUp(101), -1011);
54+
BOOST_CHECK_EQUAL(neg.EvaluateFeeUp(100000000), -1001000000);
55+
BOOST_CHECK_EQUAL(neg.EvaluateFeeUp(100000001), -1001000010);
56+
BOOST_CHECK_EQUAL(neg.EvaluateFeeUp(0x7fffffff), -21496311306);
1957

2058
BOOST_CHECK(empty == FeeFrac{}); // same as no-args
2159

@@ -67,6 +105,23 @@ BOOST_AUTO_TEST_CASE(feefrac_operators)
67105
BOOST_CHECK(oversized_1 << oversized_2);
68106
BOOST_CHECK(oversized_1 != oversized_2);
69107

108+
BOOST_CHECK_EQUAL(oversized_1.EvaluateFeeDown(0), 0);
109+
BOOST_CHECK_EQUAL(oversized_1.EvaluateFeeDown(1), 1152921);
110+
BOOST_CHECK_EQUAL(oversized_1.EvaluateFeeDown(2), 2305843);
111+
BOOST_CHECK_EQUAL(oversized_1.EvaluateFeeDown(1548031267), 1784758530396540);
112+
BOOST_CHECK_EQUAL(oversized_1.EvaluateFeeUp(0), 0);
113+
BOOST_CHECK_EQUAL(oversized_1.EvaluateFeeUp(1), 1152922);
114+
BOOST_CHECK_EQUAL(oversized_1.EvaluateFeeUp(2), 2305843);
115+
BOOST_CHECK_EQUAL(oversized_1.EvaluateFeeUp(1548031267), 1784758530396541);
116+
117+
// Test cases on the threshold where FeeFrac::Evaluate start using Mul/Div.
118+
BOOST_CHECK_EQUAL(FeeFrac(0x1ffffffff, 123456789).EvaluateFeeDown(98765432), 6871947728);
119+
BOOST_CHECK_EQUAL(FeeFrac(0x200000000, 123456789).EvaluateFeeDown(98765432), 6871947729);
120+
BOOST_CHECK_EQUAL(FeeFrac(0x200000001, 123456789).EvaluateFeeDown(98765432), 6871947730);
121+
BOOST_CHECK_EQUAL(FeeFrac(0x1ffffffff, 123456789).EvaluateFeeUp(98765432), 6871947729);
122+
BOOST_CHECK_EQUAL(FeeFrac(0x200000000, 123456789).EvaluateFeeUp(98765432), 6871947730);
123+
BOOST_CHECK_EQUAL(FeeFrac(0x200000001, 123456789).EvaluateFeeUp(98765432), 6871947731);
124+
70125
// Tests paths that use double arithmetic
71126
FeeFrac busted{(static_cast<int64_t>(INT32_MAX)) + 1, INT32_MAX};
72127
BOOST_CHECK(!(busted < busted));
@@ -77,6 +132,19 @@ BOOST_AUTO_TEST_CASE(feefrac_operators)
77132
BOOST_CHECK(max_fee <= max_fee);
78133
BOOST_CHECK(max_fee >= max_fee);
79134

135+
BOOST_CHECK_EQUAL(max_fee.EvaluateFeeDown(0), 0);
136+
BOOST_CHECK_EQUAL(max_fee.EvaluateFeeDown(1), 977888);
137+
BOOST_CHECK_EQUAL(max_fee.EvaluateFeeDown(2), 1955777);
138+
BOOST_CHECK_EQUAL(max_fee.EvaluateFeeDown(3), 2933666);
139+
BOOST_CHECK_EQUAL(max_fee.EvaluateFeeDown(1256796054), 1229006664189047);
140+
BOOST_CHECK_EQUAL(max_fee.EvaluateFeeDown(INT32_MAX), 2100000000000000);
141+
BOOST_CHECK_EQUAL(max_fee.EvaluateFeeUp(0), 0);
142+
BOOST_CHECK_EQUAL(max_fee.EvaluateFeeUp(1), 977889);
143+
BOOST_CHECK_EQUAL(max_fee.EvaluateFeeUp(2), 1955778);
144+
BOOST_CHECK_EQUAL(max_fee.EvaluateFeeUp(3), 2933667);
145+
BOOST_CHECK_EQUAL(max_fee.EvaluateFeeUp(1256796054), 1229006664189048);
146+
BOOST_CHECK_EQUAL(max_fee.EvaluateFeeUp(INT32_MAX), 2100000000000000);
147+
80148
FeeFrac max_fee2{1, 1};
81149
BOOST_CHECK(max_fee >= max_fee2);
82150

src/test/fuzz/feefrac.cpp

Lines changed: 156 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,54 +2,48 @@
22
// Distributed under the MIT software license, see the accompanying
33
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
44

5+
#include <arith_uint256.h>
6+
#include <policy/feerate.h>
57
#include <util/feefrac.h>
68
#include <test/fuzz/FuzzedDataProvider.h>
79
#include <test/fuzz/fuzz.h>
810
#include <test/fuzz/util.h>
911

1012
#include <compare>
13+
#include <cmath>
1114
#include <cstdint>
1215
#include <iostream>
1316

1417
namespace {
1518

16-
/** Compute a * b, represented in 4x32 bits, highest limb first. */
17-
std::array<uint32_t, 4> Mul128(uint64_t a, uint64_t b)
19+
/** The maximum absolute value of an int64_t, as an arith_uint256 (2^63). */
20+
const auto MAX_ABS_INT64 = arith_uint256{1} << 63;
21+
22+
/** Construct an arith_uint256 whose value equals abs(x). */
23+
arith_uint256 Abs256(int64_t x)
1824
{
19-
std::array<uint32_t, 4> ret{0, 0, 0, 0};
20-
21-
/** Perform ret += v << (32 * pos), at 128-bit precision. */
22-
auto add_fn = [&](uint64_t v, int pos) {
23-
uint64_t accum{0};
24-
for (int i = 0; i + pos < 4; ++i) {
25-
// Add current value at limb pos in ret.
26-
accum += ret[3 - pos - i];
27-
// Add low or high half of v.
28-
if (i == 0) accum += v & 0xffffffff;
29-
if (i == 1) accum += v >> 32;
30-
// Store lower half of result in limb pos in ret.
31-
ret[3 - pos - i] = accum & 0xffffffff;
32-
// Leave carry in accum.
33-
accum >>= 32;
34-
}
35-
// Make sure no overflow.
36-
assert(accum == 0);
37-
};
38-
39-
// Multiply the 4 individual limbs (schoolbook multiply, with base 2^32).
40-
add_fn((a & 0xffffffff) * (b & 0xffffffff), 0);
41-
add_fn((a >> 32) * (b & 0xffffffff), 1);
42-
add_fn((a & 0xffffffff) * (b >> 32), 1);
43-
add_fn((a >> 32) * (b >> 32), 2);
44-
return ret;
25+
if (x >= 0) {
26+
// For positive numbers, pass through the value.
27+
return arith_uint256{static_cast<uint64_t>(x)};
28+
} else if (x > std::numeric_limits<int64_t>::min()) {
29+
// For negative numbers, negate first.
30+
return arith_uint256{static_cast<uint64_t>(-x)};
31+
} else {
32+
// Special case for x == -2^63 (for which -x results in integer overflow).
33+
return MAX_ABS_INT64;
34+
}
4535
}
4636

47-
/* comparison helper for std::array */
48-
std::strong_ordering compare_arrays(const std::array<uint32_t, 4>& a, const std::array<uint32_t, 4>& b) {
49-
for (size_t i = 0; i < a.size(); ++i) {
50-
if (a[i] != b[i]) return a[i] <=> b[i];
37+
/** Construct an arith_uint256 whose value equals abs(x), for 96-bit x. */
38+
arith_uint256 Abs256(std::pair<int64_t, uint32_t> x)
39+
{
40+
if (x.first >= 0) {
41+
// x.first and x.second are both non-negative; sum their absolute values.
42+
return (Abs256(x.first) << 32) + Abs256(x.second);
43+
} else {
44+
// x.first is negative and x.second is non-negative; subtract the absolute values.
45+
return (Abs256(x.first) << 32) - Abs256(x.second);
5146
}
52-
return std::strong_ordering::equal;
5347
}
5448

5549
std::strong_ordering MulCompare(int64_t a1, int64_t a2, int64_t b1, int64_t b2)
@@ -59,23 +53,14 @@ std::strong_ordering MulCompare(int64_t a1, int64_t a2, int64_t b1, int64_t b2)
5953
int sign_b = (b1 == 0 ? 0 : b1 < 0 ? -1 : 1) * (b2 == 0 ? 0 : b2 < 0 ? -1 : 1);
6054
if (sign_a != sign_b) return sign_a <=> sign_b;
6155

62-
// Compute absolute values.
63-
uint64_t abs_a1 = static_cast<uint64_t>(a1), abs_a2 = static_cast<uint64_t>(a2);
64-
uint64_t abs_b1 = static_cast<uint64_t>(b1), abs_b2 = static_cast<uint64_t>(b2);
65-
// Use (~x + 1) instead of the equivalent (-x) to silence the linter; mod 2^64 behavior is
66-
// intentional here.
67-
if (a1 < 0) abs_a1 = ~abs_a1 + 1;
68-
if (a2 < 0) abs_a2 = ~abs_a2 + 1;
69-
if (b1 < 0) abs_b1 = ~abs_b1 + 1;
70-
if (b2 < 0) abs_b2 = ~abs_b2 + 1;
56+
// Compute absolute values of products.
57+
auto mul_abs_a = Abs256(a1) * Abs256(a2), mul_abs_b = Abs256(b1) * Abs256(b2);
7158

7259
// Compute products of absolute values.
73-
auto mul_abs_a = Mul128(abs_a1, abs_a2);
74-
auto mul_abs_b = Mul128(abs_b1, abs_b2);
7560
if (sign_a < 0) {
76-
return compare_arrays(mul_abs_b, mul_abs_a);
61+
return mul_abs_b <=> mul_abs_a;
7762
} else {
78-
return compare_arrays(mul_abs_a, mul_abs_b);
63+
return mul_abs_a <=> mul_abs_b;
7964
}
8065
}
8166

@@ -121,3 +106,128 @@ FUZZ_TARGET(feefrac)
121106
assert((fr1 == fr2) == std::is_eq(cmp_total));
122107
assert((fr1 != fr2) == std::is_neq(cmp_total));
123108
}
109+
110+
FUZZ_TARGET(feefrac_div_fallback)
111+
{
112+
// Verify the behavior of FeeFrac::DivFallback over all possible inputs.
113+
114+
// Construct a 96-bit signed value num, a positive 31-bit value den, and rounding mode.
115+
FuzzedDataProvider provider(buffer.data(), buffer.size());
116+
auto num_high = provider.ConsumeIntegral<int64_t>();
117+
auto num_low = provider.ConsumeIntegral<uint32_t>();
118+
std::pair<int64_t, uint32_t> num{num_high, num_low};
119+
auto den = provider.ConsumeIntegralInRange<int32_t>(1, std::numeric_limits<int32_t>::max());
120+
auto round_down = provider.ConsumeBool();
121+
122+
// Predict the sign of the actual result.
123+
bool is_negative = num_high < 0;
124+
// Evaluate absolute value using arith_uint256. If the actual result is negative and we are
125+
// rounding down, or positive and we are rounding up, the absolute value of the quotient is
126+
// the rounded-up quotient of the absolute values.
127+
auto num_abs = Abs256(num);
128+
auto den_abs = Abs256(den);
129+
auto quot_abs = (is_negative == round_down) ?
130+
(num_abs + den_abs - 1) / den_abs :
131+
num_abs / den_abs;
132+
133+
// If the result is not representable by an int64_t, bail out.
134+
if ((is_negative && quot_abs > MAX_ABS_INT64) || (!is_negative && quot_abs >= MAX_ABS_INT64)) {
135+
return;
136+
}
137+
138+
// Verify the behavior of FeeFrac::DivFallback.
139+
auto res = FeeFrac::DivFallback(num, den, round_down);
140+
assert(res == 0 || (res < 0) == is_negative);
141+
assert(Abs256(res) == quot_abs);
142+
143+
// Compare approximately with floating-point.
144+
long double expect = round_down ? std::floor(num_high * 4294967296.0L + num_low) / den
145+
: std::ceil(num_high * 4294967296.0L + num_low) / den;
146+
// Expect to be accurate within 50 bits of precision, +- 1 sat.
147+
if (expect == 0.0L) {
148+
assert(res >= -1 && res <= 1);
149+
} else if (expect > 0.0L) {
150+
assert(res >= expect * 0.999999999999999L - 1.0L);
151+
assert(res <= expect * 1.000000000000001L + 1.0L);
152+
} else {
153+
assert(res >= expect * 1.000000000000001L - 1.0L);
154+
assert(res <= expect * 0.999999999999999L + 1.0L);
155+
}
156+
}
157+
158+
FUZZ_TARGET(feefrac_mul_div)
159+
{
160+
// Verify the behavior of:
161+
// - The combination of FeeFrac::Mul + FeeFrac::Div.
162+
// - The combination of FeeFrac::MulFallback + FeeFrac::DivFallback.
163+
// - FeeFrac::Evaluate.
164+
165+
// Construct a 32-bit signed multiplicand, a 64-bit signed multiplicand, a positive 31-bit
166+
// divisor, and a rounding mode.
167+
FuzzedDataProvider provider(buffer.data(), buffer.size());
168+
auto mul32 = provider.ConsumeIntegral<int32_t>();
169+
auto mul64 = provider.ConsumeIntegral<int64_t>();
170+
auto div = provider.ConsumeIntegralInRange<int32_t>(1, std::numeric_limits<int32_t>::max());
171+
auto round_down = provider.ConsumeBool();
172+
173+
// Predict the sign of the overall result.
174+
bool is_negative = ((mul32 < 0) && (mul64 > 0)) || ((mul32 > 0) && (mul64 < 0));
175+
// Evaluate absolute value using arith_uint256. If the actual result is negative and we are
176+
// rounding down or positive and we rounding up, the absolute value of the quotient is the
177+
// rounded-up quotient of the absolute values.
178+
auto prod_abs = Abs256(mul32) * Abs256(mul64);
179+
auto div_abs = Abs256(div);
180+
auto quot_abs = (is_negative == round_down) ?
181+
(prod_abs + div_abs - 1) / div_abs :
182+
prod_abs / div_abs;
183+
184+
// If the result is not representable by an int64_t, bail out.
185+
if ((is_negative && quot_abs > MAX_ABS_INT64) || (!is_negative && quot_abs >= MAX_ABS_INT64)) {
186+
// If 0 <= mul32 <= div, then the result is guaranteed to be representable. In the context
187+
// of the Evaluate{Down,Up} calls below, this corresponds to 0 <= at_size <= feefrac.size.
188+
assert(mul32 < 0 || mul32 > div);
189+
return;
190+
}
191+
192+
// Verify the behavior of FeeFrac::Mul + FeeFrac::Div.
193+
auto res = FeeFrac::Div(FeeFrac::Mul(mul64, mul32), div, round_down);
194+
assert(res == 0 || (res < 0) == is_negative);
195+
assert(Abs256(res) == quot_abs);
196+
197+
// Verify the behavior of FeeFrac::MulFallback + FeeFrac::DivFallback.
198+
auto res_fallback = FeeFrac::DivFallback(FeeFrac::MulFallback(mul64, mul32), div, round_down);
199+
assert(res == res_fallback);
200+
201+
// Compare approximately with floating-point.
202+
long double expect = round_down ? std::floor(static_cast<long double>(mul32) * mul64 / div)
203+
: std::ceil(static_cast<long double>(mul32) * mul64 / div);
204+
// Expect to be accurate within 50 bits of precision, +- 1 sat.
205+
if (expect == 0.0L) {
206+
assert(res >= -1 && res <= 1);
207+
} else if (expect > 0.0L) {
208+
assert(res >= expect * 0.999999999999999L - 1.0L);
209+
assert(res <= expect * 1.000000000000001L + 1.0L);
210+
} else {
211+
assert(res >= expect * 1.000000000000001L - 1.0L);
212+
assert(res <= expect * 0.999999999999999L + 1.0L);
213+
}
214+
215+
// Verify the behavior of FeeFrac::Evaluate{Down,Up}.
216+
if (mul32 >= 0) {
217+
auto res_fee = round_down ?
218+
FeeFrac{mul64, div}.EvaluateFeeDown(mul32) :
219+
FeeFrac{mul64, div}.EvaluateFeeUp(mul32);
220+
assert(res == res_fee);
221+
222+
// Compare approximately with CFeeRate.
223+
if (mul64 <= std::numeric_limits<int64_t>::max() / 1000 &&
224+
mul64 >= std::numeric_limits<int64_t>::min() / 1000 &&
225+
quot_abs <= arith_uint256{std::numeric_limits<int64_t>::max() / 1000}) {
226+
CFeeRate feerate(mul64, (uint32_t)div);
227+
CAmount feerate_fee{feerate.GetFee(mul32)};
228+
auto allowed_gap = static_cast<int64_t>(mul32 / 1000 + 3 + round_down);
229+
assert(feerate_fee - res_fee >= -allowed_gap);
230+
assert(feerate_fee - res_fee <= allowed_gap);
231+
}
232+
}
233+
}

0 commit comments

Comments
 (0)