This infrastructure has two near term applications prior to cluster mempool:
Limited Package RBF(https://github.com/bitcoin/bitcoin/pull/28984): We want to allow package RBF only when we know it improves the mempool. This narrowly scoped functionality allows use with v3-like topologies, and will be expanded at some point post-cluster mempool when diagram checks can be done efficiently against bounded cluster sizes.
Replacement for single tx RBF(in a cluster size of up to two) against conflicts of up to cluster size two. ImprovesFeerateDiagram interface will have to change for this use-case, which is a future direction to solve certain pins and improve mempool incentive compatibility: https://delvingbitcoin.org/t/ephemeral-anchors-and-mev/383#diagram-checks-fix-this-3
And longer-term, this would be the proposed way we would compute incentive compatibility for all conflicts, post-cluster mempool.
DrahtBot
commented at 6:07 pm on January 12, 2024:
contributor
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.
Conflicts
No conflicts as of last run.
instagibbs force-pushed
on Jan 12, 2024
DrahtBot
commented at 6:29 pm on January 12, 2024:
contributor
🚧 At least one of the CI tasks failed. Make sure to run all tests locally, according to the
documentation.
Possibly this is due to a silent merge conflict (the changes in this pull request being
incompatible with the current code in the target branch). If so, make sure to rebase on the latest
commit of the target branch.
Leave a comment here, if you need help tracking down a confusing failure.
86+ {
87+ fee += other.fee;
88+ size += other.size;
89+ }
90+
91+ /** Subtrack size and size of another FeeFrac from this one. */
ismaelsadeeq
commented at 2:09 pm on January 16, 2024:
0/** Add fee and size of another FeeFrac to this one.*/1 void inline operator+=(const FeeFrac& other) noexcept
2 {
3 fee += other.fee;
4 size += other.size;
5 }
67/** Subtrack fee and size of another FeeFrac from this one.*/
instagibbs
commented at 1:14 pm on January 17, 2024:
also, Subtrack isn’t a word…
instagibbs
commented at 4:01 pm on January 19, 2024:
done
in
src/util/feefrac.cpp:12
in
ec3cf62361outdated
0@@ -0,0 +1,21 @@
1+// Copyright (c) The Bitcoin Core developers
2+// Distributed under the MIT software license, see the accompanying
3+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+#include <util/feefrac.h>
6+
7+void BuildDiagramFromUnsortedChunks(std::vector<FeeFrac>& chunks, std::vector<FeeFrac>& diagram)
8+{
9+ diagram.clear();
ismaelsadeeq
commented at 2:09 pm on January 16, 2024:
The correctness of the result of BuildDiagramFromUnsortedChunks very much depends on the assumption that the chunks can be reordered w.r.t. one another, which seems to be the case in the current PR, but will very much not be the case going forward in a general cluster mempool world.
I would suggest either:
Removing the sorting step in this function (moving it to the caller). The function can then take Span<const FeeFrac> as input too, and perhaps in a further iteration (in a later PR) this function could then be turned into one that actually performs chunking too.
Adding a big comment on the function definition that it needs independent chunks.
sdaftuar
commented at 4:13 pm on February 22, 2024:
The correctness of the result of BuildDiagramFromUnsortedChunks very much depends on the assumption that the chunks can be reordered w.r.t. one another, which seems to be the case in the current PR, but will very much not be the case going forward in a general cluster mempool world.
No objection to your suggested change here, but I don’t follow this comment – in a general cluster mempool world, unless we were to bound chunk sizes to something smaller than a cluster size (resulting in the possibility of chunk feerates not being monotonically decreasing), I think for the purposes of a feerate diagram it is fine to sort by chunk feerate and permit equal-feerate-chunks to be reordered? From the perspective of a feerate diagram, two such diagrams are equivalent.
If you’re suggesting that we want to preserve the ability in the future to impose a chunk size limit, then yes I agree that we should rewrite this… Is there another case that I’m overlooking?
instagibbs
commented at 5:57 pm on February 22, 2024:
Removing the sorting step in this function (moving it to the caller). The function can then take Span as input too, and perhaps in a further iteration (in a later PR) this function could then be turned into one that actually performs chunking too.
Took this suggestion
in
src/test/feefrac_tests.cpp:17
in
8d74a28a62outdated
I can think of one use case where negative sizes are actually useful, though it’s pretty far fetched.
Currently the code hardcodes a “tail feerate” (the feerate of which we assume there is an infinite supply) of 0 sat/vB for mempool improvement checks, but in theory this can be configurable. In that case, in addition to the diagram check between old and new package, we want the property that fee_new >= fee_old + (size_new - size_old) * feerate_tail. If old, new, and tail are CFeeFrac objects, this condition is exactly !((new - old) << tail). If new is smaller than old, the new - old object has negative size.
A more pragmatic answer is that there is no real reason to restrict it to just positive sizes, and not enforcing it is simpler.
sdaftuar
commented at 4:54 pm on February 14, 2024:
If old, new, and tail are CFeeFrac objects, this condition is exactly !((new - old) << tail). If new is smaller than old, the new - old object has negative size.
@sipa With the various Assume() calls that check that if the size is 0, the fee must also be zero, doesn’t that mean that we can’t really write code like this example you gave? Unless we checked that new != old first – otherwise you might create a FeeFrac with 0 size and non-zero fee.
@sdaftuar Indeed. It wouldn’t require anything more than dropping those Assume() calls, but right now FeeFrac objects represent (the aggregate fee and size of) sets of transactions, rather differences between such aggregates (as would be needed for this use case).
sdaftuar
commented at 3:42 pm on February 17, 2024:
I think we should drop the Assume() calls, as otherwise I think we ought to do more to ensure that using operator- on FeeFrac’s is safe everywhere that it might be invoked.
in
src/txmempool.cpp:1305
in
ab9a952bddoutdated
1300+
1301+ // new_diagram will have chunks that consist of each ancestor of
1302+ // direct_conflicts that is at its own fee/size, along with the replacement
1303+ // tx/package at its own fee/size
1304+
1305+ // old diagram will consist of each element of direct_conflicts either at
ismaelsadeeq
commented at 3:36 pm on January 16, 2024:
I think you mean all_conflicts here
0 // old diagram will consist of each element of all_conflicts either at
instagibbs
commented at 4:01 pm on January 19, 2024:
done
in
src/txmempool.cpp:1327
in
ab9a952bddoutdated
1322+ // Does this transaction have ancestors?
1323+ FeeFrac individual{txiter->GetModifiedFee(), txiter->GetTxSize()};
1324+ if (txiter->GetCountWithAncestors() > 1) {
1325+ // We'll add chunks for either the ancestor by itself and this tx
1326+ // by itself, or for a combined package.
1327+ FeeFrac package{txiter->GetModFeesWithAncestors(), int32_t(txiter->GetSizeWithAncestors())};
ismaelsadeeq
commented at 3:42 pm on January 16, 2024:
ismaelsadeeq
commented at 3:52 pm on January 16, 2024:
I understand what happens here but its a bit confusing to me the short forms CON, CNKDIAGRAM
Will prefer something like the compute in computing the OLD chunk
0// Step 2: build the new diagram
1 2// Add any parents of direct conflicts that are not conflicted themselves into the NEW chunk
3for (auto direct_conflict : direct_conflicts) {
4// If a direct conflict has an ancestor that is notin all_conflicts,
5// it can be affected by the replacement of the child. 6if (direct_conflict->GetMemPoolParentsConst().size() >0) {
7// Grab the parent. 8const CTxMemPoolEntry& parent = direct_conflict->GetMemPoolParentsConst().begin()->get();
9if (!all_conflicts.count(mapTx.iterator_to(parent))) {
10// This transaction would be left over, so add to the NEW
11// chunk.12 new_chunks.emplace_back(parent.GetModifiedFee(), parent.GetTxSize());
13 }
14 }
15 }
16// Add the replacement package to NEW
17 new_chunks.emplace_back(replacement_fees, int32_t(replacement_vsize));
instagibbs
commented at 12:21 pm on January 17, 2024:
I’d like to keep track of these ideas somehow, maybe by defining the labels?
ismaelsadeeq
commented at 1:17 pm on January 17, 2024:
Yeah, maybe to be consistent define the labels and do the same in the OLDchunk computation loop above.
Though the explicit comments in the OLDchunk computation loop is more easier to parse for me :).
instagibbs
commented at 4:01 pm on January 19, 2024:
took some of hte language, and explicitly defined each term
ismaelsadeeq
commented at 4:23 pm on January 16, 2024:
Should forward the detailed error message also?
instagibbs
commented at 12:23 pm on January 17, 2024:
can you be more explicit in suggestion? The errors are being returned?
ismaelsadeeq
commented at 1:11 pm on January 17, 2024:
I meant what is being returned by err_string and CompareFeerateDiagram was not used. why were you unable to compute mining score, same for CompareFeerateDiagram why is the fee rate insufficient.
instagibbs
commented at 2:08 pm on January 17, 2024:
CompareFeerateDiagram won’t return much very interesting, but you’re right for the err_string, will return it
instagibbs
commented at 4:01 pm on January 19, 2024:
done
instagibbs force-pushed
on Jan 18, 2024
DrahtBot added the label
CI failed
on Jan 18, 2024
DrahtBot
commented at 8:29 pm on January 18, 2024:
contributor
🚧 At least one of the CI tasks failed. Make sure to run all tests locally, according to the
documentation.
Possibly this is due to a silent merge conflict (the changes in this pull request being
incompatible with the current code in the target branch). If so, make sure to rebase on the latest
commit of the target branch.
Leave a comment here, if you need help tracking down a confusing failure.
123+ CAmount replacement_fees,
124+ int64_t replacement_vsize)
125+ EXCLUSIVE_LOCKS_REQUIRED(pool.cs);
126+
127+/** Compares two feerate diagrams. The shorter one is padded with a horizonal line. */
128+std::partial_ordering CompareFeerateDiagram(std::vector<FeeFrac>& dia0, std::vector<FeeFrac>& dia1);
Suggested change that (I believe) avoids the need for suppressions (and may be more readable?):
0auto add_fn = [&](uint64_t v, int pos) {
1uint64_t accum{0};
2for (int i =0; i + pos <4; ++i) {
3// Add current value at limb pos in ret.
4 accum += ret[3- pos - i];
5// Add low or high half of v.
6if (i ==0) accum += v &0xffffffff;
7if (i ==1) accum += v >>32;
8// Store lower half of result in limb pos in ret.
9 ret[3- pos - i] = accum &0xffffffff;
10// Leave carry in accum.
11 accum >>=32;
12 }
13// Make sure no overflow.
14 assert(accum ==0);
15 };
instagibbs
commented at 8:25 pm on January 19, 2024:
looks good, done
in
src/test/fuzz/feefrac.cpp:62
in
a4b92cfd64outdated
Self-nit: operator<, operator>, operator<= and operator>= can be dropped (C++20 will autogenerate them using operator<=> (inspecting the compiled code, it’s marginally less efficient, but all the actually performance-critical uses use operator<<, operator>>, or FeeRateCompare anyway).
instagibbs
commented at 8:25 pm on January 19, 2024:
done
DrahtBot removed the label
CI failed
on Jan 19, 2024
instagibbs force-pushed
on Jan 19, 2024
instagibbs force-pushed
on Jan 19, 2024
instagibbs force-pushed
on Jan 19, 2024
DrahtBot added the label
CI failed
on Jan 19, 2024
DrahtBot
commented at 8:32 pm on January 19, 2024:
contributor
🚧 At least one of the CI tasks failed. Make sure to run all tests locally, according to the
documentation.
Possibly this is due to a silent merge conflict (the changes in this pull request being
incompatible with the current code in the target branch). If so, make sure to rebase on the latest
commit of the target branch.
Leave a comment here, if you need help tracking down a confusing failure.
murchandamus
commented at 7:42 pm on January 22, 2024:
In the commit message of “Implement ImprovesFeerateDiagram” (9257f33d259bf68f1df1cc41b7b3caaea341782f):
This new function takes the populated sets of
direct and all conflicts computed in the current
mempool, assuming the replacements are a single
chunk, and computes a diagram check.
The first sentence is confusing to me. Could you perhaps clarify “the populated sets of direct and all conflicts” and split the sentence up?
instagibbs
commented at 1:26 pm on January 23, 2024:
direct_conflicts and all_conflicts. Does that help?
murchandamus
commented at 7:05 pm on January 23, 2024:
Yeah, that would improve it
in
src/txmempool.cpp:1264
in
9257f33d25outdated
1270+ } else if (has_ancestor && has_descendant) {
1271+ return strprintf("%s has both ancestor and descendant", txid_string);
1272+ }
1273+ // Additionally enforce that:
1274+ // If we have a parent, we are its only child.
1275+ // If we have a child, we are its only parent.
murchandamus
commented at 7:55 pm on January 22, 2024:
In “Implement ImprovesFeerateDiagram” (9257f33d259bf68f1df1cc41b7b3caaea341782f):
Nit: The documentation is in opposite order as the checks. Perhaps switch these two lines.
instagibbs
commented at 1:52 pm on January 23, 2024:
done
in
src/txmempool.cpp:1271
in
9257f33d25outdated
1266+ if (ancestor_count > 2) {
1267+ return strprintf("%s has %u ancestors, max 1 allowed", txid_string, ancestor_count - 1);
1268+ } else if (descendant_count > 2) {
1269+ return strprintf("%s has %u descendants, max 1 allowed", txid_string, descendant_count - 1);
1270+ } else if (has_ancestor && has_descendant) {
1271+ return strprintf("%s has both ancestor and descendant", txid_string);
murchandamus
commented at 7:57 pm on January 22, 2024:
In “Implement ImprovesFeerateDiagram” (9257f33d259bf68f1df1cc41b7b3caaea341782f):
0 return strprintf("%s has both ancestor and descendant, exceeding cluster limit of 2", txid_string);
instagibbs
commented at 1:52 pm on January 23, 2024:
taken
in
src/policy/rbf.h:122
in
9257f33d25outdated
114@@ -106,4 +115,20 @@ std::optional<std::string> PaysForRBF(CAmount original_fees,
115 CFeeRate relay_fee,
116 const uint256& txid);
117118+/**
119+ * The replacement transaction must improve the feerate diagram of the mempool.
120+ * @param[in] pool The mempool.
121+ * @param[in] direct_conflicts Set of txids corresponding to the direct conflicts
122+ * @param[in] all_conflicts Set of mempool entries corresponding to all conflicts
murchandamus
commented at 8:12 pm on January 22, 2024:
In “Implement ImprovesFeerateDiagram” (9257f33d259bf68f1df1cc41b7b3caaea341782f):
Maybe that’s my unfamiliarity with the mempool code, but it is not obvious to me what “direct_conflicts” and “all_conflicts” are, and the description here is self-referential. Do these transactions belong to original, replacement, or both, etc.?
instagibbs
commented at 1:30 pm on January 23, 2024:
direct_conflicts -> the set of transactions that have at least one input conflicting with a proposed transaction.
all_conflicts -> Everything that would be evicted by the proposed transaction
I’ll touch this up a bit but this is the nomenclature elsewhere
in
src/txmempool.cpp:1335
in
9257f33d25outdated
1330+ // therefore higher than the parent's fee. Chunk these
1331+ // together.
1332+ old_chunks.emplace_back(package);
1333+ } else {
1334+ // Add two points, one for the parent and one for this child.
1335+ old_chunks.emplace_back(package-individual);
murchandamus
commented at 8:34 pm on January 22, 2024:
In “Implement ImprovesFeerateDiagram” (9257f33d259bf68f1df1cc41b7b3caaea341782f):
Nit: Spaces around that minus? It took me a moment to realize that dashes aren’t a thing in variables.
instagibbs
commented at 1:51 pm on January 23, 2024:
fixed
in
src/txmempool.h:744
in
9257f33d25outdated
739+ * Calculate the old and new mempool feerate diagrams relating to the
740+ * clusters that would be affected by a potential replacement transaction.
741+ *
742+ * @param[in] replacement_fees Package fees
743+ * @param[in] replacement_vsize Package size
744+ * @param[in] direct_conflicts All transactions that would be removed directly (not via descendants)
murchandamus
commented at 8:55 pm on January 22, 2024:
In “Implement ImprovesFeerateDiagram” (9257f33d259bf68f1df1cc41b7b3caaea341782f):
0 * [@param](/bitcoin-bitcoin/contributor/param/)[in] direct_conflicts All transactions that would be removed directly (not by being descendants of conflicting transactions)
instagibbs
commented at 1:51 pm on January 23, 2024:
tried my own explanation
in
src/test/rbf_tests.cpp:296
in
b3d415fe84outdated
294+ old_diagram = {FeeFrac{0, 0}, FeeFrac{950, 300}, FeeFrac{1050, 400}};
295+ new_diagram = {FeeFrac{0, 0}, FeeFrac{1100, 300}};
296+
297+ BOOST_CHECK(std::is_lt(CompareFeerateDiagram(old_diagram, new_diagram)));
298+
299+ // Feerate of first chunk is sufficiently better, but second chunk is worse.
murchandamus
commented at 9:40 pm on January 22, 2024:
In “test: Add tests for CompareFeerateDiagram and CheckConflictTopology” (b3d415fe84be7edfbed567a79a25d406b438622b):
Nit: I found the comment here confusing. How about: “new_diagram is strictly better due to the first chunk, while the second chunk is worse.”
instagibbs
commented at 1:51 pm on January 23, 2024:
tried my own re-explanation. I agree as-is is confusing
in
src/test/rbf_tests.cpp:304
in
b3d415fe84outdated
302+
303+ BOOST_CHECK(std::is_lt(CompareFeerateDiagram(old_diagram, new_diagram)));
304+
305+ // Feerate of first chunk is better, but second chunk is worse
306+ old_diagram = {FeeFrac{0, 0}, FeeFrac{950, 300}, FeeFrac{1050, 400}};
307+ new_diagram = {FeeFrac{0, 0}, FeeFrac{750, 100}, FeeFrac{999, 350}, FeeFrac{1150, 500}};
murchandamus
commented at 9:44 pm on January 22, 2024:
In “test: Add tests for CompareFeerateDiagram and CheckConflictTopology” (b3d415fe84be7edfbed567a79a25d406b438622b):
Nit: in the new diagram, the last two chunks should be one chunk because the last chunk is better (151 sats, 150 vB) than the prior (249 sats, 250 vB).
instagibbs
commented at 1:51 pm on January 23, 2024:
ah hmm, I’m not sure that matters for correctness of test, but jacked up the last chunk’s size just to make it clearer what we’re testing
in
src/test/rbf_tests.cpp:328
in
b3d415fe84outdated
326+
327+ // No change in evaluation when tail check needed.
328+ BOOST_CHECK(std::is_gt(CompareFeerateDiagram(old_diagram, new_diagram)));
329+
330+ old_diagram = {FeeFrac{0, 0}, FeeFrac{950, 300}, FeeFrac{1050, 399}};
331+
murchandamus
commented at 9:51 pm on January 22, 2024:
In “test: Add tests for CompareFeerateDiagram and CheckConflictTopology” (b3d415fe84be7edfbed567a79a25d406b438622b):
This is the same old_diagram. Did you mean to repeat the same transactions,
740+ * clusters that would be affected by a potential replacement transaction.
741+ *
742+ * @param[in] replacement_fees Package fees
743+ * @param[in] replacement_vsize Package size
744+ * @param[in] direct_conflicts All transactions that would be removed directly by
745+ * having a conflicting input wih a proposed transaction
murchandamus
commented at 7:09 pm on January 23, 2024:
0 * having a conflicting input with a proposed transaction
instagibbs
commented at 1:37 pm on January 25, 2024:
fixed
in
src/test/rbf_tests.cpp:509
in
a14b95129doutdated
instagibbs
commented at 3:07 pm on January 24, 2024:
member
@dergoegge fuzzer created a circular reference which caused incongruities between descendant counts and actually walking the descendants in mempool. Please run again?
dergoegge
commented at 4:03 pm on January 24, 2024:
member
ismaelsadeeq
commented at 4:37 pm on January 29, 2024:
0 int64_t fee;
1 int32_t size;
Should their be more information that will describe the transaction size and fee, base / modified fee, vsize in bytes or something of that sort.
If not I think its okay like this.
Unrelated just asking to learn.
Why are’nt we using CAmount here for the fee?
In some places I see transaction size as uint32_t while some places its int32_t.
Should we have a type for size just like CAmount?
instagibbs
commented at 3:54 pm on February 12, 2024:
If not I think its okay like this.
removed comments, how it’s precisely used is an implementation detail outside of this code
murchandamus
commented at 6:22 pm on February 2, 2024:
contributor
ACKa1414b3388a870aeb463e9cc293d47cbd86e7f0e
via rangediff a14b95129d3a2894b7a41ce919a426bb60f62e35..a1414b3388a870aeb463e9cc293d47cbd86e7f0e
The changes since my prior ACK appear to only pertain to improvements in the fuzz test to avoid circular packages and a typo fix on a comment in the code.
DrahtBot added the label
Needs rebase
on Feb 10, 2024
instagibbs force-pushed
on Feb 12, 2024
instagibbs
commented at 3:56 pm on February 12, 2024:
member
rebased
DrahtBot removed the label
Needs rebase
on Feb 12, 2024
ismaelsadeeq
commented at 5:16 pm on February 13, 2024:
In implementation they are sorted in increasing fee rate and the chunks in the comments should be ordered as is? and also the 0 fee and size chucks sorts first.
0 * by decreasing size. The empty FeeFrac (fee and size both 0) sorts first. So for example, the
1 * following FeeFracs are in sorted order:
2 *
3 * - fee=0 size=0 (undefined feerate)
4 * - fee=2 size=1 (feerate 2)
5 * - fee=3 size=2 (feerate 1.5)
6 * - fee=1 size=1 (feerate 1)
7 * - fee=2 size=2 (feerate 1)
8 * - fee=2 size=3 (feerate 0.667...)
9 * - fee=1 size=2 (feerate 0.5)
10 * - fee=0 size=1 (feerate 0)
instagibbs
commented at 8:44 pm on February 14, 2024:
I don’t understand your comment, sorry. They’re listed in increasing feerate, with decreasing size as a tie breaker.
ismaelsadeeq
commented at 1:50 pm on February 15, 2024:
Yes but in reverse I think.
Suppose to be the other way around no?
A sorted FeeFracs should have undefined chunks first, the highest fee rate chunk second continuously…
If their is a tie, the chunk with lower size comes first.
Hence the are sorted in increasing fee rates, and then by decreasing size.
@ismaelsadeeq The comparator you provide to std::sort is supposed to return whether the first argument should sort before the second one (if no comparator is provided, operator< is used). With that, you should see lowest feerate first, highest feerate last, and the undefined feefrac at the very end. Within equal-feerate groups, larger size comes first.
ismaelsadeeq
commented at 2:00 pm on February 15, 2024:
Yes thanks for clarification 👍🏾
ismaelsadeeq
commented at 2:05 pm on February 15, 2024:
I looked at it the other way around, using the result from BuildDiagramFromUnsortedChunks sort which uses custom > operator @instagibbs.
instagibbs
commented at 4:53 pm on February 15, 2024:
I added some additional feefrac fuzzing for the +/- operations, marking as resolved
instagibbs
commented at 5:14 pm on February 16, 2024:
this is odd, should we just Assume()replacement_vsize>0 in CalculateFeerateDiagramsForRBF?
ismaelsadeeq
commented at 7:41 pm on February 16, 2024:
Yes, I can’t think of a scenario where that will happen!
instagibbs
commented at 5:10 pm on February 20, 2024:
Added Assume for this and remove test
in
src/test/rbf_tests.cpp:326
in
d57fbda350outdated
321+ const CAmount low_fee{CENT/100};
322+ const CAmount normal_fee{CENT/10};
323+ const CAmount high_fee{CENT};
324+
325+ // low -> high -> medium fee transactions that would result in two chunks together
326+ const auto low_tx= make_tx(/*inputs=*/ {m_coinbase_txns[0]}, /*output_values=*/ {10 * COIN});
ismaelsadeeq
commented at 7:42 pm on February 16, 2024:
nit:
0const auto low_tx = make_tx(/*inputs=*/ {m_coinbase_txns[0]}, /*output_values=*/ {10* COIN});
ismaelsadeeq
commented at 10:44 am on February 17, 2024:
here an another place below!
instagibbs
commented at 5:10 pm on February 20, 2024:
fixed
in
src/test/rbf_tests.cpp:495
in
d57fbda350outdated
Yeah, it means “fee fraction”, though the name isn’t perfect - it really represents any ratio where the numerator is an int64_t and the denominator is an int32_t, with a sort order that tie-breaks by putting larger denominators last.
I don’t think Chunk is a good name, as we’re using that term for a subset of transactions. A FeeFrac is related, but represents the aggregate feerate of a chunk (or really of any set of transactions, or even the difference between the fees/sizes of two sets of transactions).
instagibbs
commented at 5:09 pm on February 20, 2024:
@sdaftuar going to hold off on this for now if that’s ok
in
src/util/feefrac.cpp:51
in
d200d30431outdated
46+ const int unproc_side = next_point(0).size > next_point(1).size;
47+
48+ // let `P` be the next point on diagram unproc_side, and `A` and `B` the previous and next points
49+ // on the other diagram. We want to know if P lies above or below the line AB. To determine this, we
50+ // compute the direction coefficients of line AB and of line AP, and compare them. These
51+ // direction coefficients are fee per size, and can thus be expressed as FeeFracs.
sdaftuar
commented at 3:48 pm on February 17, 2024:
nit: “direction coefficient” is not a term I’m familiar with (and from googling it doesn’t seem like a super common term); perhaps it would be clearer to just use the word “slope” here?
Oh, I seem to have translated literally from Dutch. “Slope” is indeed the proper English translation.
instagibbs
commented at 5:09 pm on February 20, 2024:
replaced, even though I thought it sounded pretty smart
in
src/util/feefrac.h:159
in
d200d30431outdated
154+
155+/** Takes the pre-computed chunks and generates a fee diagram which starts at FeeFrac of (0, 0) */
156+void BuildDiagramFromUnsortedChunks(std::vector<FeeFrac>& chunks, std::vector<FeeFrac>& diagram);
157+
158+/** Compares two feerate diagrams (which must both start at size=0). The shorter one is implicitly
159+ * extended with a horizontal straight line. */
sdaftuar
commented at 7:20 pm on February 17, 2024:
nit: Could add a comment here more precisely defining the preconditions for this function and what definition we are using for feerate diagram: specifically that 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). (If these conditions are violated then the function can fail with an Assume() failure.)
instagibbs
commented at 5:09 pm on February 20, 2024:
taken with some liberties
in
src/policy/rbf.cpp:193
in
802a104d0eoutdated
188+ const CTxMemPool::setEntries& direct_conflicts,
189+ const CTxMemPool::setEntries& all_conflicts,
190+ CAmount replacement_fees,
191+ int64_t replacement_vsize)
192+{
193+ // Require that the replacement strictly improve the mempool's fee vs. size diagram.
sdaftuar
commented at 7:25 pm on February 17, 2024:
nit: “fee vs. size diagram” – maybe we should standardize the terminology to “feerate diagram”? I was also thinking about adding some documentation to doc/policy/mempool-replacements.md explaining what we’re doing now.
instagibbs
commented at 5:09 pm on February 20, 2024:
changed to standard lingo
in
src/txmempool.h:757
in
802a104d0eoutdated
745+ * @param[in] all_conflicts All transactions that would be removed
746+ * @param[out] old_diagram The feerate diagram of the relevant clusters before accepting the new tx
747+ * @param[out] new_diagram The feerate diagram of the relevant clusters after accepting the new tx
748+ * @return An optional error string if the conflicts don't match a calculable topology
749+ */
750+ std::optional<std::string> CalculateFeerateDiagramsForRBF(CAmount replacement_fees, int64_t replacement_vsize, const setEntries& direct_conflicts, const setEntries& all_conflicts, std::vector<FeeFrac>& old_diagram, std::vector<FeeFrac>& new_diagram) EXCLUSIVE_LOCKS_REQUIRED(cs);
sdaftuar
commented at 8:43 pm on February 17, 2024:
nit: Maybe update the comment to be more explicit that this function only works for the situation where (replacement_fees, replacement_size) corresponds to a transaction package that has no in-mempool dependences.
instagibbs
commented at 5:09 pm on February 20, 2024:
updated to be more explicit on what those values represent
sdaftuar
commented at 8:59 pm on February 17, 2024:
member
Still reviewing all the tests – leaving some nits that I’ve noticed so far.
In addition:
I commented here that I think we should drop the Assume(size != 0 || fee == 0) calls in feefrac.h, so that we don’t have to police all the places we invoke operator- to make sure we can never accidentally violate that.
Another nit: in the commit message for commit 860823fb93212fa78ab928589bd403da462be222, it should say “FeeFrac” and not “FeeFrace”
68+ if (point_b.size == point_p.size) ++next_index[!unproc_side];
69+ }
70+
71+ // Tail check at 0 feerate: Compare the remaining area. Use similar logic as in the loop above,
72+ // except we use a horizontal line instead of AB, as no point B exists anymore.
73+ const int long_side = next_index[1] != dias[1].size();
Here is a patch that merges the two processing loops into one (deduplicating some logic between them), and also makes the tail feerate explicit (even though only FeeFrac{0, 1} is used for now).
No need to take this, but in case people find it simpler to review a generic approach, perhaps it’s useful.
0diff --git a/src/util/feefrac.cpp b/src/util/feefrac.cpp
1index a970adedc30..569f32f4615 100644
2--- a/src/util/feefrac.cpp
3+++ b/src/util/feefrac.cpp
4@@ -22,7 +22,7 @@ void BuildDiagramFromUnsortedChunks(std::vector<FeeFrac>& chunks, std::vector<Fe
5 }
6 }
7 8-std::partial_ordering CompareFeerateDiagram(Span<const FeeFrac> dia0, Span<const FeeFrac> dia1)
9+std::partial_ordering CompareFeerateDiagram(Span<const FeeFrac> dia0, Span<const FeeFrac> dia1, FeeFrac tail_feerate)
10 {
11 /** Array to allow indexed access to input diagrams. */
12 const std::array<Span<const FeeFrac>, 2> dias = {dia0, dia1};
13@@ -40,51 +40,47 @@ std::partial_ordering CompareFeerateDiagram(Span<const FeeFrac> dia0, Span<const
14 Assert(prev_point(0).IsEmpty());
15 Assert(prev_point(1).IsEmpty());
16 17- // Compare the overlapping area of the diagrams.
18- while (next_index[0] < dias[0].size() && next_index[1] < dias[1].size()) {
19- // Determine which diagram has the first unprocessed point.
20- const int unproc_side = next_point(0).size > next_point(1).size;
21+ do {
22+ bool done_0 = next_index[0] == dias[0].size();
23+ bool done_1 = next_index[1] == dias[1].size();
24+ if (done_0 && done_1) break;
25+
26+ // Determine which diagram has the first unprocessed point. If one side is finished, use the
27+ // other one.
28+ const int unproc_side = (done_0 || done_1) ? done_0 : next_point(0).size > next_point(1).size;
29 30 // let `P` be the next point on diagram unproc_side, and `A` and `B` the previous and next points
31 // on the other diagram. We want to know if P lies above or below the line AB. To determine this, we
32- // compute the direction coefficients of line AB and of line AP, and compare them. These
33- // direction coefficients are fee per size, and can thus be expressed as FeeFracs.
34+ // compute the slopes of line AB and of line AP, and compare them. These slopes are fee per size,
35+ // and can thus be expressed as FeeFracs.
36 const FeeFrac& point_p = next_point(unproc_side);
37 const FeeFrac& point_a = prev_point(!unproc_side);
38- const FeeFrac& point_b = next_point(!unproc_side);
39- const auto coef_ab = point_b - point_a;
40- const auto coef_ap = point_p - point_a;
41- Assume(coef_ap.size > 0);
42- Assume(coef_ab.size >= coef_ap.size);
43- // Perform the comparison. If P lies above AB, unproc_side is better in P. If P lies below
44- // AB, then !unproc_side is better in P.
45- const auto cmp = FeeRateCompare(coef_ap, coef_ab);
46+ const auto slope_ap = point_p - point_a;
47+ Assume(slope_ap.size > 0);
48+ std::weak_ordering cmp = std::weak_ordering::equivalent;
49+ if (!(done_0 || done_1)) {
50+ // If both sides have points left, compute B, and the slope of AB explicitly.
51+ const FeeFrac& point_b = next_point(!unproc_side);
52+ const auto slope_ab = point_b - point_a;
53+ Assume(slope_ab.size >= slope_ap.size);
54+ cmp = FeeRateCompare(slope_ap, slope_ab);
55+
56+ // If B and P have the same size, B can be marked as processed (in addition to P, see
57+ // below), as we've already performed a comparison at this size.
58+ if (point_b.size == point_p.size) ++next_index[!unproc_side];
59+ } else {
60+ // If one of the sides has no points left, act as if AB has slope tail_feerate.
61+ cmp = FeeRateCompare(slope_ap, tail_feerate);
62+ }
63+
64+ // If P lies above AB, unproc_side is better in P. If P lies below AB, then !unproc_side is
65+ // better in P.
66 if (std::is_gt(cmp)) better_somewhere[unproc_side] = true;
67 if (std::is_lt(cmp)) better_somewhere[!unproc_side] = true;
68 69- // Mark P as processed. If B and P have the same size, B can also be marked as processed as
70- // we've already performed a comparison at this size.
71- ++next_index[unproc_side];
72- if (point_b.size == point_p.size) ++next_index[!unproc_side];
73- }
74-
75- // Tail check at 0 feerate: Compare the remaining area. Use similar logic as in the loop above,
76- // except we use a horizontal line instead of AB, as no point B exists anymore.
77- const int long_side = next_index[1] != dias[1].size();
78- Assume(next_index[!long_side] == dias[!long_side].size());
79- // The point A now remains fixed: the last point of the shorter diagram.
80- const FeeFrac& point_a = prev_point(!long_side);
81- while (next_index[long_side] < dias[long_side].size()) {
82- // Compare AP (where P is the next unprocessed point on the longer diagram) with a horizontal line
83- // extending infinitely from A. This is equivalent to checking the sign of the fee of P-A.
84- const FeeFrac& point_p = next_point(long_side);
85- const auto coef_ap = point_p - point_a;
86- const auto cmp = coef_ap.fee <=> 0;
87- if (std::is_gt(cmp)) better_somewhere[long_side] = true;
88- if (std::is_lt(cmp)) better_somewhere[!long_side] = true;
89 // Mark P as processed.
90- ++next_index[long_side];
91- }
92+ ++next_index[unproc_side];
93+ } while(true);
94 95 // If both diagrams are better somewhere, they are incomparable.
96 if (better_somewhere[0] && better_somewhere[1]) return std::partial_ordering::unordered;
97diff --git a/src/util/feefrac.h b/src/util/feefrac.h
98index 12698a4a653..706facc34b0 100644
99--- a/src/util/feefrac.h
100+++ b/src/util/feefrac.h
101@@ -156,7 +156,7 @@ struct FeeFrac
102 void BuildDiagramFromUnsortedChunks(std::vector<FeeFrac>& chunks, std::vector<FeeFrac>& diagram);
103104 /** Compares two feerate diagrams (which must both start at size=0). The shorter one is implicitly
105- * extended with a horizontal straight line. */
106-std::partial_ordering CompareFeerateDiagram(Span<const FeeFrac> dia0, Span<const FeeFrac> dia1);
107+ * extended with an infinite line whose slope equals tail_feefrac (by default: horizontal). */
108+std::partial_ordering CompareFeerateDiagram(Span<const FeeFrac> dia0, Span<const FeeFrac> dia1, FeeFrac tail_feerate={0, 1});
109110 #endif // BITCOIN_UTIL_FEEFRAC_H
instagibbs
commented at 5:09 pm on February 20, 2024:
took in a modified form since it does seem to read cleaner to me, left the direct exposure of tail_feerate as a YNGNI extension
in
src/txmempool.h:760
in
802a104d0eoutdated
748+ * @return An optional error string if the conflicts don't match a calculable topology
749+ */
750+ std::optional<std::string> CalculateFeerateDiagramsForRBF(CAmount replacement_fees, int64_t replacement_vsize, const setEntries& direct_conflicts, const setEntries& all_conflicts, std::vector<FeeFrac>& old_diagram, std::vector<FeeFrac>& new_diagram) EXCLUSIVE_LOCKS_REQUIRED(cs);
751+
752+ /* Check that all direct conflicts are in a cluster size of two or less. */
753+ std::optional<std::string> CheckConflictTopology(const setEntries& direct_conflicts);
Question: is the idea that in the future we’ll use the same function but loosen it beyond cluster <=2? Otherwise, I wonder why it isn’t called LimitTopology1P1C or LimitClusterSize2 or something.
instagibbs
commented at 2:58 pm on March 12, 2024:
Potentially yes. I suspect it will become fully unrestricted next post-cluster mempool, but we don’t know anything for sure.
instagibbs force-pushed
on Feb 20, 2024
instagibbs
commented at 5:10 pm on February 20, 2024:
member
I commented #29242 (review) that I think we should drop the Assume(size != 0 || fee == 0) calls in feefrac.h, so that we don’t have to police all the places we invoke operator- to make sure we can never accidentally violate that.
Another nit: in the commit message for commit https://github.com/bitcoin/bitcoin/commit/860823fb93212fa78ab928589bd403da462be222, it should say “FeeFrac” and not “FeeFrace”
Addressed these by removing Assumes and updating the commit text
in
src/test/feefrac_tests.cpp:102
in
947693bc8eoutdated
We have Shuffle() in random.h, which is more efficient than std::shuffle (but only works with FastRandomContext).
instagibbs
commented at 5:48 pm on February 22, 2024:
removed, since I stripped out the sorting function from BuildDiagramFromUnsortedChunks
in
src/util/feefrac.cpp:21
in
4f050b23f3outdated
16+
17+ // And now build the diagram for these chunks.
18+ diagram.emplace_back(0, 0);
19+ for (auto& chunk : chunks) {
20+ FeeFrac& last = diagram.back();
21+ diagram.emplace_back(last.fee+chunk.fee, last.size+chunk.size);
instagibbs
commented at 5:48 pm on February 22, 2024:
done
in
src/util/feefrac.cpp:56
in
4f050b23f3outdated
54+ // compute the slopes of line AB and of line AP, and compare them. These slopes are fee per size,
55+ // and can thus be expressed as FeeFracs.
56+ const FeeFrac& point_p = next_point(unproc_side);
57+ const FeeFrac& point_a = prev_point(!unproc_side);
58+
59+ // Slope of AP can be negative, unlike AB
sdaftuar
commented at 3:49 pm on February 22, 2024:
I think the slope of AB can be negative, because a user could use the prioritisetransaction to give a transaction a negative fee?
instagibbs
commented at 3:53 pm on February 22, 2024:
one weird trick! will remove the comment
willcl-ark
commented at 10:40 am on March 20, 2024:
Suhas’ comment is still present.
instagibbs
commented at 12:07 pm on March 25, 2024:
un-resolved comment(oops), will add to follow-up
instagibbs
commented at 4:14 pm on March 25, 2024:
opened in follow-up
sipa
commented at 3:54 pm on February 22, 2024:
member
Partial review.
hebasto
commented at 5:14 pm on February 22, 2024:
member
Concept ACK on introducing feerate diagram checks for a subset of RBF transactions.
instagibbs force-pushed
on Feb 22, 2024
instagibbs force-pushed
on Feb 22, 2024
in
src/test/rbf_tests.cpp:423
in
55eae36cfboutdated
418+ BOOST_CHECK(new_diagram[0] == FeeFrac(0, 0));
419+ BOOST_CHECK(new_diagram[1] == FeeFrac(high_fee, low_size_2));
420+ old_diagram.clear();
421+ new_diagram.clear();
422+
423+ // You can have more than two direct conflicts if the therea re multiple effected clusters, all of size 2 or less
ismaelsadeeq
commented at 12:03 pm on March 4, 2024:
0 // You can have more than two direct conflicts if the there are multiple effected clusters, all of size 2 or less
instagibbs
commented at 1:49 pm on March 11, 2024:
done
in
src/test/feefrac_tests.cpp:24
in
cc7b730683outdated
nit: I see you use BOOST_CHECK(x == y) a lot. BOOST_CHECK_EQUAL is nice since it prints more helpfully
instagibbs
commented at 4:31 pm on March 12, 2024:
seems I have to implement overloaded operator std::ostream& operator<<(std::ostream& os, const FeeFrac& type) to support it, and seems somewhat rare in the codebase. I can do it of course. Thoughts?
nit: you skipped err_string2, maybe you could give them descriptive names like result_cpfp_replace_cpfp, result_replace_3genresult_replace_3independent, etc.
instagibbs
commented at 3:33 pm on March 13, 2024:
an attempt was made!
in
src/txmempool.h:759
in
5f6ddba263outdated
754+ * @param[out] new_diagram The feerate diagram of the relevant clusters after accepting the new tx
755+ * @return An optional error string if the conflicts don't match a calculable topology
756+ */
757+ std::optional<std::string> CalculateFeerateDiagramsForRBF(CAmount replacement_fees, int64_t replacement_vsize, const setEntries& direct_conflicts, const setEntries& all_conflicts, std::vector<FeeFrac>& old_diagram, std::vector<FeeFrac>& new_diagram) EXCLUSIVE_LOCKS_REQUIRED(cs);
758+
759+ /* Check that all direct conflicts are in a cluster size of two or less. */
5f6ddba2635ca2100ed46560f338b64a58f61b73 Maybe mention can contain multiple clusters?
instagibbs
commented at 3:33 pm on March 13, 2024:
done
in
src/txmempool.h:757
in
5f6ddba263outdated
752+ * @param[in] all_conflicts All transactions that would be removed
753+ * @param[out] old_diagram The feerate diagram of the relevant clusters before accepting the new tx
754+ * @param[out] new_diagram The feerate diagram of the relevant clusters after accepting the new tx
755+ * @return An optional error string if the conflicts don't match a calculable topology
756+ */
757+ std::optional<std::string> CalculateFeerateDiagramsForRBF(CAmount replacement_fees, int64_t replacement_vsize, const setEntries& direct_conflicts, const setEntries& all_conflicts, std::vector<FeeFrac>& old_diagram, std::vector<FeeFrac>& new_diagram) EXCLUSIVE_LOCKS_REQUIRED(cs);
Did you consider using util::Result for this instead? Seems like you want std::pair<std::vector<FeeFrac>, std::vector<FeeFrac>> on success or a std::string on failure.
instagibbs
commented at 3:33 pm on March 13, 2024:
done
in
src/util/feefrac.h:150
in
d2025b37cdoutdated
145+ std::swap(a.size, b.size);
146+ }
147+};
148+
149+/** Takes the pre-computed and topologically-valid chunks and generates a fee diagram which starts at FeeFrac of (0, 0) */
150+std::vector<FeeFrac> BuildDiagramFromChunks(const Span<const FeeFrac> chunks);
Nit: the const before Span<const FeeFrac> doesn’t do anything (it’s ignored in the declaration of by-value function arguments). You can have it be const in the function definition (in feefrac.cpp) without having it const here (where it does have the effect that the function body cannot modify the copy it receives).
instagibbs
commented at 2:26 pm on March 18, 2024:
I probably should just be passing a reference here anyways? Changed to pass by reference
The general advice is to pass arguments not larger than 2 pointers by value, which is the case for spans.
instagibbs
commented at 2:35 pm on March 18, 2024:
got it, double checked, taking your suggestion to remove const from declaration instead
in
src/txmempool.cpp:1278
in
5f6ddba263outdated
1273+ const auto& our_parent = direct_conflict->GetMemPoolParentsConst().begin();
1274+ if (our_parent->get().GetCountWithDescendants() > 2) {
1275+ return strprintf("%s is not the only child of parent %s",
1276+ txid_string, our_parent->get().GetSharedTx()->GetHash().ToString());
1277+ }
1278+ }
I see you’ve added coverage for the “not the only parent” case, but I don’t see a test for “not the only child” case. Also nothing failed when I commented it out.
instagibbs
commented at 3:34 pm on March 19, 2024:
I presume you mean the multi-child case didn’t trigger failure when removed? multi-parent should
glozow
commented at 2:50 pm on March 12, 2024:
member
The code looks good, I’m still reading through the tests
in
src/test/rbf_tests.cpp:281
in
70ffb25852outdated
276+ TestMemPoolEntryHelper entry;
277+
278+ const CAmount low_fee{CENT/100};
279+ const CAmount normal_fee{CENT/10};
280+
281+ // One low feerate txn followed be normal feerate
0 // Adding a grandchild makes the cluster size 3, which is uncalculable
instagibbs
commented at 3:33 pm on March 13, 2024:
we could expand acceptable topologies later. Took language.
in
src/test/fuzz/rbf.cpp:107
in
5661fbaec3outdated
102+ if (!mtx) return;
103+
104+ CTxMemPool pool{MemPoolOptionsForTest(g_setup->m_node)};
105+
106+ // Add a bunch of parent-child pairs to the mempool, and remember them.
107+ std::vector<CTransaction> txs;
Is there any reason to unlock and lock in the loop? Why not just hold throughout?
instagibbs
commented at 3:34 pm on March 13, 2024:
changed to hold it throughout
in
src/test/fuzz/rbf.cpp:43
in
5661fbaec3outdated
38+ static const auto testing_setup = MakeNoLogFileContext<>();
39+ g_setup = testing_setup.get();
40+
41+ // Create a fixed set of unique "UTXOs" to source parents from
42+ // to avoid fuzzer giving circular references
43+ for (int i = 0; i < NUM_ITERS * 2; ++i) {
Why make this ahead of time instead of just creating a new CMutableTransaction for each child?
instagibbs
commented at 3:34 pm on March 13, 2024:
Aping the current rbf tests, probably. Picks a valid transaction structure, then sticks with it for the run. Suggestions for follow-up improvements maybe?
in
src/test/fuzz/feeratediagram.cpp:92
in
f4e7c2748doutdated
124+ }
125+}
126+
127+FUZZ_TARGET(build_and_compare_feerate_diagram)
128+{
129+ // Generate a random set of chunks
DrahtBot removed the label
CI failed
on Mar 14, 2024
in
src/txmempool.cpp:1305
in
e88d6ceff0outdated
1300+
1301+ std::vector<FeeFrac> old_chunks;
1302+ // Step 1: build the old diagram.
1303+
1304+ // The above clusters are all trivially linearized;
1305+ // they have a strict topology of 1 or two connected transactions.
murchandamus
commented at 7:19 pm on March 14, 2024:
Nit:
0 // they have a strict topology of one or two connected transactions.
or
0 // they have a strict topology of 1 or 2 connected transactions.
instagibbs
commented at 4:14 pm on March 25, 2024:
opened in follow-up
in
src/policy/rbf.cpp:198
in
e88d6ceff0outdated
193+ // Require that the replacement strictly improve the mempool's feerate diagram.
194+ std::vector<FeeFrac> old_diagram, new_diagram;
195+
196+ const auto diagram_results{pool.CalculateFeerateDiagramsForRBF(replacement_fees, replacement_vsize, direct_conflicts, all_conflicts)};
197+
198+ if (!diagram_results.has_value()) {
I’m confused about this. If diagram_results has no value, does it make sense to pass it to util::ErrorString below?
instagibbs
commented at 2:04 pm on March 18, 2024:
util::Result lets you pass an error string in lieu of a std::nullopt, and based on the docs it seems I’m using this right. @glozow ?
0//! `std::optional<T>` can be updated to return `util::Result<T>` and return
1//! error strings usually just replacing `return std::nullopt;` with `return
2//! util::Error{error_string};`.
But you aren’t passing it an error string, you’re passing it diagram_results, which is at this stage known to be std::nullopt?
Nevermind, I missed that diagram_results is an util::Result, not an std::optional, so the !diagram_results.has_value() means it does contain an error. Sorry!
instagibbs
commented at 2:15 pm on March 18, 2024:
instagibbs
commented at 2:26 pm on March 18, 2024:
done
sipa
commented at 1:45 pm on March 17, 2024:
member
Here is a commit that removes the need to explicitly compute the diagram, and instead makes the diagram comparison operate directly on a (sorted) list of chunks: https://github.com/sipa/bitcoin/commits/pr29242
Feel free to ignore, postpone, include, or squash.
instagibbs
commented at 2:07 pm on March 18, 2024:
member
@sipa to keep things moving (muh package rbf) I’ll postpone/ promise to review your PR for that commit as a follow-up.
This new function takes the populated sets of
direct and all conflicts computed in the current
mempool, assuming the replacements are a single
chunk, and computes a diagram check.
The diagram check only works against cluster
sizes of 2 or less, and fails if it encounters
a different topology.
Co-authored-by: Suhas Daftuar <sdaftuar@chaincode.com>
test: unit test for ImprovesFeerateDiagramb767e6bd47
Unit tests for CalculateFeerateDiagramsForRBF7295986778
instagibbs force-pushed
on Mar 18, 2024
in
src/util/feefrac.h:34
in
ce8e22542eoutdated
29+ * - fee=0 size=0 (undefined feerate)
30+ *
31+ * A FeeFrac is considered "better" if it sorts after another, by this ordering. All standard
32+ * comparison operators (<=>, ==, !=, >, <, >=, <=) respect this ordering.
33+ *
34+ * The CompareFeeFrac, and >> and << operators only compare feerate and treat equal feerate but
b767e6bd47cb0fb8f7aea3fb10c597e59a35bf74 This is perhaps the place to add coverage for the fact that modified feerate is used, e.g. PrioritiseTransaction here and check that using a replacement_fee between modified and base makes a difference in the result.
instagibbs
commented at 4:15 pm on March 25, 2024:
opened in follow-up
in
src/test/fuzz/rbf.cpp:124
in
588a98dcccoutdated
72959867784098137a50c34f86deca8235eef4f8
Nit, but this old_diagram, new_diagram assignment setup is
making a copies of everything
brittle / easy for the tests to bleed into each other, e.g. if you forgot to reset the variables (and, previously, if you forgot to clear them between tests)
repetitive
checking size + each element of a vector when you could just compare it to an expected vector
0 // With one less vB it does
1 BOOST_CHECK(ImprovesFeerateDiagram(pool, {entry1}, {entry1, entry2}, tx1_fee + tx2_fee, tx1_size + tx2_size - 1) == std::nullopt);
instagibbs
commented at 4:15 pm on March 25, 2024:
opened in follow-up
in
src/test/fuzz/rbf.cpp:177
in
588a98dcccoutdated
172+ assert(calc_results->first.back().size - replaced_size + replacement_vsize == calc_results->second.back().size);
173+ }
174+
175+ // If internals report error, wrapper should too
176+ auto err_tuple{ImprovesFeerateDiagram(pool, direct_conflicts, all_conflicts, replacement_fees, replacement_vsize)};
177+ if (!calc_results.has_value()) assert(err_tuple.has_value());
I think we can be a bit more specific than this, i.e. if !calc_results.has_value() then err_tuple.value().first == DiagramCheckError::UNCALCULABLE
instagibbs
commented at 4:14 pm on March 25, 2024:
opened in follow-up
in
src/test/fuzz/rbf.cpp:172
in
588a98dcccoutdated
167+ replaced_size += txiter->GetTxSize();
168+ }
169+ // The total fee of the new diagram should be the total fee of the old
170+ // diagram - replaced_fee + replacement_fees
171+ assert(calc_results->first.back().fee - replaced_fee + replacement_fees == calc_results->second.back().fee);
172+ assert(calc_results->first.back().size - replaced_size + replacement_vsize == calc_results->second.back().size);
This test for ImprovesFeerateDiagram doesn’t seem very strict beyond checking NEW = OLD - CON + replacement and that internal errors = wrapper errors. For example, if I swap !is_gt for a !is_eq in ImprovesFeerateDiagram, this fuzzer is still happy with everything. There is coverage for that in other places, but it seems like we could be writing stronger assertions?
Can we perhaps add a check that the result is consistent with what CompareFeerateDiagram says when comparing the diagrams returned in calc_results? Or some kind of sanity check that when the new diagram ends with lower fees, we should be getting DiagramCheckError::FAILURE?
instagibbs
commented at 4:14 pm on March 25, 2024:
I added a couple more checks where I could clearly add them in followup
glozow
commented at 2:57 pm on March 19, 2024:
member
I’ll answer because this is my code: I consider this class to be a low-level utility class for representing monetary fractions, without needing a dependency on consensus code.
murchandamus
commented at 6:51 pm on March 19, 2024:
In “Implement ImprovesFeerateDiagram” (2079b80854e2595f6f696e7c13a56c7f2a7da9f4):
I’m not sure I understand why there are “No topology restrictions post-chunking”. I am guessing that this is alluding to all affected clusters at most having two transactions and therefore each having only two possible configurations: child has higher feerate and the cluster is one chunk, or child has lower feerate and the cluster has two chunks, but the chunk with the child naturally sorts after the chunk with the parent. If that’s what is being alluded to here, perhaps you could elaborate in the comment if you touch this again.
Also, I have one open question here. It is my understanding that we do not combine two transactions into one chunk if they tie in feerate. If we have a parent and a child that have the same feerate, and the child is smaller, would that not mean that the chunk with the child could be sorted in front of the chunk with the parent incorrectly?
Thinking more about this, I think this does not matter here, because even if the child were sorted in front of the parent, given them having the same feerate, it would result in the same feerate diagram.
Also, it seems to me that this problem could be sidestepped by chunking together a parent and a child on equal feerate for the purpose of the diagram, if you used if (individual >= package) { in line 1320 instead of if (individual > package) {.
It’d need to be if (!(individual << package)) {, because >= and > use size as tie breaker when comparing FeeFracs. I think that’s generally an improvement, actually, because size shouldn’t matter here.
instagibbs
commented at 8:32 pm on March 19, 2024:
If that’s what is being alluded to here, perhaps you could elaborate in the comment if you touch this again.
Will elaborate a bit on the chunk construction comments in a follow-up.
I think that’s generally an improvement, actually, because size shouldn’t matter here.
Agreed, at a minimum I’d change to individual >> package to get rid of the tie-breaking behavior which is confusing to have here. Will take on a chosen change in follow-up.
instagibbs
commented at 4:14 pm on March 25, 2024:
opened in follow-up with >> as noted
in
src/util/feefrac.h:64
in
ce8e22542eoutdated
59+#else
60+ static constexpr auto Mul = MulFallback;
61+#endif
62+
63+ int64_t fee;
64+ int32_t size;
murchandamus
commented at 7:04 pm on March 19, 2024:
In “Add FeeFrac utils” (ce8e22542ed0b4fa5794d3203207146418d59473):
Nit: I noticed that in CalculateFeerateDiagramsForRBF(…) you explicitly use vsize, but here you use size. Perhaps vsize should be used consistently? Using vsize for the FeeFrac member would also have the added benefit that the member name doesn’t collide with the use of size referring to the count of objects in the diagram.
Hmm, I was hoping that this class could be used in contexts where the size is WU rather than vsize too. I do agree with the confusion w.r.t. the size() member functions.
Should they perhaps just be called numerator and denominator?
instagibbs
commented at 8:32 pm on March 19, 2024:
Should they perhaps just be called numerator and denominator?
I’ll mull this over a bit
murchandamus
commented at 8:33 pm on March 19, 2024:
If it were weight that would also differentiate well.
instagibbs
commented at 8:38 pm on March 19, 2024:
I think that would be even more confusing; if we’re going to change it we need it to be unit-neutral (punting for this PR)
in
src/test/fuzz/feeratediagram.cpp:29
in
4d6528a3d6outdated
24+{
25+ assert(diagram.size() > 0);
26+ unsigned not_above = 0;
27+ unsigned not_below = diagram.size() - 1;
28+ // If outside the range of diagram, extend begin/end.
29+ if (size < diagram[not_above].size) return {diagram[not_above].fee, 1};
murchandamus
commented at 7:15 pm on March 19, 2024:
In “fuzz: fuzz diagram creation and comparison” (4d6528a3d6bf3821c216c68f99170e2faab5d63c):
Nit: The use of vsize throughout would help differentiate the mixed occurrences of diagram.size() and diagram[i].size
instagibbs
commented at 4:14 pm on March 25, 2024:
leaving as is
in
src/test/rbf_tests.cpp:448
in
7295986778
443+ BOOST_CHECK(old_diagram[1] == FeeFrac(high_fee, high_size_2));
444+ BOOST_CHECK(old_diagram[2] == FeeFrac(low_fee + high_fee, low_size_2 + high_size_2));
445+ BOOST_CHECK(new_diagram[0] == FeeFrac(0, 0));
446+ BOOST_CHECK(new_diagram[1] == FeeFrac(high_fee, low_size_2));
447+
448+ // You can have more than two direct conflicts if the there are multiple effected clusters, all of size 2 or less
murchandamus
commented at 7:26 pm on March 19, 2024:
Nit: In “Unit tests for CalculateFeerateDiagramsForRBF” (72959867784098137a50c34f86deca8235eef4f8):
0 // You can have more than two direct conflicts if the there are multiple affected clusters, all of size 2 or less
instagibbs
commented at 4:14 pm on March 25, 2024:
opened in follow-up
in
src/test/rbf_tests.cpp:350
in
7295986778
345+
346+ const CAmount low_fee{CENT/100};
347+ const CAmount normal_fee{CENT/10};
348+ const CAmount high_fee{CENT};
349+
350+ // low -> high -> medium fee transactions that would result in two chunks together
murchandamus
commented at 7:40 pm on March 19, 2024:
In “Unit tests for CalculateFeerateDiagramsForRBF” (72959867784098137a50c34f86deca8235eef4f8):
Nit: this comment appears to make the unstated assumption that the three transactions are of similar vsize
instagibbs
commented at 4:14 pm on March 25, 2024:
opened in follow-up
murchandamus
commented at 7:42 pm on March 19, 2024:
contributor
utACK72959867784098137a50c34f86deca8235eef4f8
I also only have non-blocking nits
ismaelsadeeq
commented at 9:32 am on March 20, 2024:
member
189+ const CTxMemPool::setEntries& all_conflicts,
190+ CAmount replacement_fees,
191+ int64_t replacement_vsize)
192+{
193+ // Require that the replacement strictly improve the mempool's feerate diagram.
194+ std::vector<FeeFrac> old_diagram, new_diagram;
willcl-ark
commented at 11:33 am on March 20, 2024:
In “Implement ImprovesFeerateDiagram”: 2079b80854e2595f6f696e7c13a56c7f2a7da9f4
0 std::vector<FeeFrac> old_diagram, new_diagram;
These vectors at src/policy/rbf.cpp:L194 appear to be unused and the diagrams are built inside of CalculateFeerateDiagramsForRBF.
instagibbs
commented at 12:05 pm on March 25, 2024:
leftovers, will remove on touchup/followup
instagibbs
commented at 4:14 pm on March 25, 2024:
opened in follow-up
willcl-ark approved
willcl-ark
commented at 1:43 pm on March 20, 2024:
member
crACK72959867784098137a50c34f86deca8235eef4f8
This looks good on a first pass of the code, and was a good introduction (to me) for how we are approaching implementation of the Diagrammatic fee rate checks, which matches my current understanding of the proposed concept.
Left only two small non-blocking nits.
In addition, while checking each commit I did notice that clang-format could be run on most (all?) of them too if wanted (but I’d guess it’s not wanted at this stage of review 😄 ).
achow101 assigned glozow
on Mar 20, 2024
instagibbs
commented at 4:14 pm on March 25, 2024:
member
I see you’ve added coverage for the “not the only parent” case, but I don’t see a test for “not the only child” case. Also nothing failed when I commented it out.
@glozow can’t figure out github interface, I added a test in followup
in
src/util/feefrac.cpp:80
in
ce8e22542eoutdated
75+ // If P lies above AB, unproc_side is better in P. If P lies below AB, then !unproc_side is
76+ // better in P.
77+ if (std::is_gt(cmp)) better_somewhere[unproc_side] = true;
78+ if (std::is_lt(cmp)) better_somewhere[!unproc_side] = true;
79+ ++next_index[unproc_side];
80+ } while(true);
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:
0} while (!(better_somewhere[0] && better_somewhere[1]));
instagibbs
commented at 12:40 pm on March 26, 2024:
I think slightly better is to pull the check a few lines below into the loop, just after updating the better_somewhere array:
1294+ // direct_conflicts that is at its own fee/size, along with the replacement
1295+ // tx/package at its own fee/size
1296+
1297+ // old diagram will consist of each element of all_conflicts either at
1298+ // its own feerate (followed by any descendant at its own feerate) or as a
1299+ // single chunk at its descendant's ancestor feerate.
I think this comment is wrong – it should say something like:
0// old diagram will consist of the ancestors and descendants of each element of
1// all_conflicts. every such transaction will either be at its own feerate (followed
2// by any descendant at its own feerate), or as a single chunk at the descendant's
3// ancestor feerate.
instagibbs
commented at 12:42 pm on March 26, 2024:
taken, indeed my definition missed the ancestors
sdaftuar approved
sdaftuar
commented at 7:54 pm on March 25, 2024:
member
ACK72959867784098137a50c34f86deca8235eef4f8
Left some nits that can be addressed (or not) in a followup.
glozow merged this
on Mar 26, 2024
glozow closed this
on Mar 26, 2024
glozow unassigned glozow
on Mar 26, 2024
achow101 referenced this in commit
61de64df67
on Mar 29, 2024
glozow referenced this in commit
2a07c4662d
on Apr 24, 2024
This is a metadata mirror of the GitHub repository
bitcoin/bitcoin.
This site is not affiliated with GitHub.
Content is generated from a GitHub metadata backup.
generated: 2025-06-20 21:13 UTC
This site is hosted by @0xB10C More mirrored repositories can be found on mirror.b10c.me