Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mempool util: Add RBF diagram checks for single chunks against clusters of size 2 #29242

Merged
merged 9 commits into from
Mar 26, 2024
3 changes: 3 additions & 0 deletions src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ BITCOIN_CORE_H = \
util/error.h \
util/exception.h \
util/fastrange.h \
util/feefrac.h \
util/fees.h \
util/fs.h \
util/fs_helpers.h \
Expand Down Expand Up @@ -741,6 +742,7 @@ libbitcoin_util_a_SOURCES = \
util/check.cpp \
util/error.cpp \
util/exception.cpp \
util/feefrac.cpp \
util/fees.cpp \
util/fs.cpp \
util/fs_helpers.cpp \
Expand Down Expand Up @@ -983,6 +985,7 @@ libbitcoinkernel_la_SOURCES = \
util/batchpriority.cpp \
util/chaintype.cpp \
util/check.cpp \
util/feefrac.cpp \
util/fs.cpp \
util/fs_helpers.cpp \
util/hasher.cpp \
Expand Down
86 changes: 86 additions & 0 deletions src/util/feefrac.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) The Bitcoin Core developers
instagibbs marked this conversation as resolved.
Show resolved Hide resolved
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <util/feefrac.h>
#include <algorithm>
#include <array>
#include <vector>

std::vector<FeeFrac> BuildDiagramFromChunks(const Span<const FeeFrac> chunks)
{
std::vector<FeeFrac> diagram;
diagram.reserve(chunks.size() + 1);

diagram.emplace_back(0, 0);
for (auto& chunk : chunks) {
diagram.emplace_back(diagram.back() + chunk);
}
return diagram;
}

std::partial_ordering CompareFeerateDiagram(Span<const FeeFrac> dia0, Span<const FeeFrac> dia1)
{
/** Array to allow indexed access to input diagrams. */
const std::array<Span<const FeeFrac>, 2> dias = {dia0, dia1};
/** How many elements we have processed in each input. */
size_t next_index[2] = {1, 1};
/** Whether the corresponding input is strictly better than the other at least in one place. */
bool better_somewhere[2] = {false, false};
/** Get the first unprocessed point in diagram number dia. */
const auto next_point = [&](int dia) { return dias[dia][next_index[dia]]; };
/** Get the last processed point in diagram number dia. */
const auto prev_point = [&](int dia) { return dias[dia][next_index[dia] - 1]; };

// Diagrams should be non-empty, and first elements zero in size and fee
Assert(!dia0.empty() && !dia1.empty());
Assert(prev_point(0).IsEmpty());
Assert(prev_point(1).IsEmpty());

do {
bool done_0 = next_index[0] == dias[0].size();
bool done_1 = next_index[1] == dias[1].size();
if (done_0 && done_1) break;

// Determine which diagram has the first unprocessed point. If a single side is finished, use the
// other one. Only up to one can be done due to check above.
const int unproc_side = (done_0 || done_1) ? done_0 : next_point(0).size > next_point(1).size;

// Let `P` be the next point on diagram unproc_side, and `A` and `B` the previous and next points
// on the other diagram. We want to know if P lies above or below the line AB. To determine this, we
// compute the slopes of line AB and of line AP, and compare them. These slopes are fee per size,
// and can thus be expressed as FeeFracs.
const FeeFrac& point_p = next_point(unproc_side);
const FeeFrac& point_a = prev_point(!unproc_side);

// Slope of AP can be negative, unlike AB
instagibbs marked this conversation as resolved.
Show resolved Hide resolved
const auto slope_ap = point_p - point_a;
Assume(slope_ap.size > 0);
std::weak_ordering cmp = std::weak_ordering::equivalent;
if (done_0 || done_1) {
// If a single side has no points left, act as if AB has slope tail_feerate(of 0).
Assume(!(done_0 && done_1));
cmp = FeeRateCompare(slope_ap, FeeFrac(0, 1));
} else {
// If both sides have points left, compute B, and the slope of AB explicitly.
const FeeFrac& point_b = next_point(!unproc_side);
const auto slope_ab = point_b - point_a;
Assume(slope_ab.size >= slope_ap.size);
cmp = FeeRateCompare(slope_ap, slope_ab);

// If B and P have the same size, B can be marked as processed (in addition to P, see
// below), as we've already performed a comparison at this size.
if (point_b.size == point_p.size) ++next_index[!unproc_side];
}
// If P lies above AB, unproc_side is better in P. If P lies below AB, then !unproc_side is
// better in P.
if (std::is_gt(cmp)) better_somewhere[unproc_side] = true;
if (std::is_lt(cmp)) better_somewhere[!unproc_side] = true;
++next_index[unproc_side];
} while(true);
Copy link
Member

Choose a reason for hiding this comment

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

Just noticed that I think we could change this to short circuit in the case where we've detected that both diagrams are "better somewhere" and hence incomparable, eg:

} while (!(better_somewhere[0] && better_somewhere[1]));

Copy link
Member Author

Choose a reason for hiding this comment

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

I think slightly better is to pull the check a few lines below into the loop, just after updating the better_somewhere array:

if (better_somewhere[0] && better_somewhere[1]) return std::partial_ordering::unordered;

?

Copy link
Member Author

Choose a reason for hiding this comment

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

e.g., a9d42b9


// If both diagrams are better somewhere, they are incomparable.
if (better_somewhere[0] && better_somewhere[1]) return std::partial_ordering::unordered;
// Otherwise compare the better_somewhere values.
return better_somewhere[0] <=> better_somewhere[1];
}
160 changes: 160 additions & 0 deletions src/util/feefrac.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Copyright (c) The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#ifndef BITCOIN_UTIL_FEEFRAC_H
#define BITCOIN_UTIL_FEEFRAC_H

#include <stdint.h>
#include <compare>
#include <vector>
#include <span.h>
#include <util/check.h>

/** Data structure storing a fee and size, ordered by increasing fee/size.
*
* The size of a FeeFrac cannot be zero unless the fee is also zero.
*
* FeeFracs have a total ordering, first by increasing feerate (ratio of fee over size), and then
* by decreasing size. The empty FeeFrac (fee and size both 0) sorts last. So for example, the
* following FeeFracs are in sorted order:
*
* - fee=0 size=1 (feerate 0)
* - fee=1 size=2 (feerate 0.5)
* - fee=2 size=3 (feerate 0.667...)
* - fee=2 size=2 (feerate 1)
* - fee=1 size=1 (feerate 1)
* - fee=3 size=2 (feerate 1.5)
* - fee=2 size=1 (feerate 2)
* - fee=0 size=0 (undefined feerate)
*
instagibbs marked this conversation as resolved.
Show resolved Hide resolved
* A FeeFrac is considered "better" if it sorts after another, by this ordering. All standard
* comparison operators (<=>, ==, !=, >, <, >=, <=) respect this ordering.
*
* The CompareFeeFrac, and >> and << operators only compare feerate and treat equal feerate but
Copy link
Member

Choose a reason for hiding this comment

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

In commit "Add FeeFrac utils":

Self-nit: CompareFeeFrac -> FeeRateCompare

Copy link
Member Author

Choose a reason for hiding this comment

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

opened in follow-up

* different size as equivalent. The empty FeeFrac is neither lower or higher in feerate than any
* other.
*/
struct FeeFrac
{
/** Fallback version for Mul (see below).
*
* Separate to permit testing on platforms where it isn't actually needed.
*/
static inline std::pair<int64_t, uint32_t> MulFallback(int64_t a, int32_t b) noexcept
{
// Otherwise, emulate 96-bit multiplication using two 64-bit multiplies.
int64_t low = int64_t{static_cast<uint32_t>(a)} * b;
int64_t high = (a >> 32) * b;
return {high + (low >> 32), static_cast<uint32_t>(low)};
}

// Compute a * b, returning an unspecified but totally ordered type.
#ifdef __SIZEOF_INT128__
static inline __int128 Mul(int64_t a, int32_t b) noexcept
{
// If __int128 is available, use 128-bit wide multiply.
return __int128{a} * b;
}
#else
static constexpr auto Mul = MulFallback;
#endif

int64_t fee;
instagibbs marked this conversation as resolved.
Show resolved Hide resolved
int32_t size;
instagibbs marked this conversation as resolved.
Show resolved Hide resolved

/** Construct an IsEmpty() FeeFrac. */
inline FeeFrac() noexcept : fee{0}, size{0} {}

/** Construct a FeeFrac with specified fee and size. */
inline FeeFrac(int64_t f, int32_t s) noexcept : fee{f}, size{s} {}

inline FeeFrac(const FeeFrac&) noexcept = default;
inline FeeFrac& operator=(const FeeFrac&) noexcept = default;

/** Check if this is empty (size and fee are 0). */
bool inline IsEmpty() const noexcept {
return size == 0;
}

/** Add fee and size of another FeeFrac to this one. */
void inline operator+=(const FeeFrac& other) noexcept
{
fee += other.fee;
size += other.size;
}

/** Subtract fee and size of another FeeFrac from this one. */
void inline operator-=(const FeeFrac& other) noexcept
{
fee -= other.fee;
size -= other.size;
}

/** Sum fee and size. */
friend inline FeeFrac operator+(const FeeFrac& a, const FeeFrac& b) noexcept
{
return {a.fee + b.fee, a.size + b.size};
}

/** Subtract both fee and size. */
friend inline FeeFrac operator-(const FeeFrac& a, const FeeFrac& b) noexcept
{
return {a.fee - b.fee, a.size - b.size};
}

/** Check if two FeeFrac objects are equal (both same fee and same size). */
friend inline bool operator==(const FeeFrac& a, const FeeFrac& b) noexcept
{
return a.fee == b.fee && a.size == b.size;
}

/** Compare two FeeFracs just by feerate. */
friend inline std::weak_ordering FeeRateCompare(const FeeFrac& a, const FeeFrac& b) noexcept
{
auto cross_a = Mul(a.fee, b.size), cross_b = Mul(b.fee, a.size);
return cross_a <=> cross_b;
}

/** Check if a FeeFrac object has strictly lower feerate than another. */
friend inline bool operator<<(const FeeFrac& a, const FeeFrac& b) noexcept
{
auto cross_a = Mul(a.fee, b.size), cross_b = Mul(b.fee, a.size);
return cross_a < cross_b;
}

/** Check if a FeeFrac object has strictly higher feerate than another. */
friend inline bool operator>>(const FeeFrac& a, const FeeFrac& b) noexcept
{
auto cross_a = Mul(a.fee, b.size), cross_b = Mul(b.fee, a.size);
return cross_a > cross_b;
}

/** Compare two FeeFracs. <, >, <=, and >= are auto-generated from this. */
friend inline std::strong_ordering operator<=>(const FeeFrac& a, const FeeFrac& b) noexcept
{
auto cross_a = Mul(a.fee, b.size), cross_b = Mul(b.fee, a.size);
if (cross_a == cross_b) return b.size <=> a.size;
return cross_a <=> cross_b;
}

/** Swap two FeeFracs. */
friend inline void swap(FeeFrac& a, FeeFrac& b) noexcept
{
std::swap(a.fee, b.fee);
std::swap(a.size, b.size);
}
};

/** Takes the pre-computed and topologically-valid chunks and generates a fee diagram which starts at FeeFrac of (0, 0) */
std::vector<FeeFrac> BuildDiagramFromChunks(Span<const FeeFrac> chunks);

/** Compares two feerate diagrams. The shorter one is implicitly
* extended with a horizontal straight line.
*
* A feerate diagram consists of a list of (fee, size) points with the property that size
* is strictly increasing and that the first entry is (0, 0).
*/
std::partial_ordering CompareFeerateDiagram(Span<const FeeFrac> dia0, Span<const FeeFrac> dia1);

#endif // BITCOIN_UTIL_FEEFRAC_H