Merge bitcoin/bitcoin#29757: feefrac: avoid explicitly computing diagram; compare based on chunks

b22901dfa9 Avoid explicitly computing diagram; compare based on chunks (Pieter Wuille)

Pull request description:

  This merges the `BuildDiagramFromChunks` and `CompareFeeRateDiagram` introduced in #29242 into a single `CompareChunks` function, which operates on sorted chunk data rather than diagrams, instead computing the diagram on the fly.

  This avoids the need for the construction of an intermediary diagram object, and removes the slightly arbitrary "all diagrams must start at (0, 0)" requirement.

  Not a big deal, but I think the result is a bit cleaner and not really more complicated.

ACKs for top commit:
  glozow:
    reACK b22901d
  instagibbs:
    reACK b22901dfa9

Tree-SHA512: ca37bdf61d9a9cb5435f4da73e97ead33bf65828ad9af49b87336b1ece70db8ced1c21f517fc6eb6d616311c91f3da75ecae6b9bd42547133e3a3c5320b7816d
This commit is contained in:
glozow 2024-04-24 16:39:54 +01:00
commit 2a07c4662d
No known key found for this signature in database
GPG Key ID: BA03F4DBE0C63FB4
9 changed files with 142 additions and 192 deletions

View File

@ -191,13 +191,13 @@ std::optional<std::pair<DiagramCheckError, std::string>> ImprovesFeerateDiagram(
int64_t replacement_vsize)
{
// Require that the replacement strictly improves the mempool's feerate diagram.
const auto diagram_results{pool.CalculateFeerateDiagramsForRBF(replacement_fees, replacement_vsize, direct_conflicts, all_conflicts)};
const auto chunk_results{pool.CalculateChunksForRBF(replacement_fees, replacement_vsize, direct_conflicts, all_conflicts)};
if (!diagram_results.has_value()) {
return std::make_pair(DiagramCheckError::UNCALCULABLE, util::ErrorString(diagram_results).original);
if (!chunk_results.has_value()) {
return std::make_pair(DiagramCheckError::UNCALCULABLE, util::ErrorString(chunk_results).original);
}
if (!std::is_gt(CompareFeerateDiagram(diagram_results.value().second, diagram_results.value().first))) {
if (!std::is_gt(CompareChunks(chunk_results.value().second, chunk_results.value().first))) {
return std::make_pair(DiagramCheckError::FAILURE, "insufficient feerate: does not improve feerate diagram");
}
return std::nullopt;

View File

@ -82,43 +82,4 @@ BOOST_AUTO_TEST_CASE(feefrac_operators)
}
BOOST_AUTO_TEST_CASE(build_diagram_test)
{
FeeFrac p1{1000, 100};
FeeFrac empty{0, 0};
FeeFrac zero_fee{0, 1};
FeeFrac oversized_1{4611686000000, 4000000};
FeeFrac oversized_2{184467440000000, 100000};
// Diagram-building will reorder chunks
std::vector<FeeFrac> chunks;
chunks.push_back(p1);
chunks.push_back(zero_fee);
chunks.push_back(empty);
chunks.push_back(oversized_1);
chunks.push_back(oversized_2);
// Caller in charge of sorting chunks in case chunk size limit
// differs from cluster size limit
std::sort(chunks.begin(), chunks.end(), [](const FeeFrac& a, const FeeFrac& b) { return a > b; });
// Chunks are now sorted in reverse order (largest first)
BOOST_CHECK(chunks[0] == empty); // empty is considered "highest" fee
BOOST_CHECK(chunks[1] == oversized_2);
BOOST_CHECK(chunks[2] == oversized_1);
BOOST_CHECK(chunks[3] == p1);
BOOST_CHECK(chunks[4] == zero_fee);
std::vector<FeeFrac> generated_diagram{BuildDiagramFromChunks(chunks)};
BOOST_CHECK(generated_diagram.size() == 1 + chunks.size());
// Prepended with an empty, then the chunks summed in order as above
BOOST_CHECK(generated_diagram[0] == empty);
BOOST_CHECK(generated_diagram[1] == empty);
BOOST_CHECK(generated_diagram[2] == oversized_2);
BOOST_CHECK(generated_diagram[3] == oversized_2 + oversized_1);
BOOST_CHECK(generated_diagram[4] == oversized_2 + oversized_1 + p1);
BOOST_CHECK(generated_diagram[5] == oversized_2 + oversized_1 + p1 + zero_fee);
}
BOOST_AUTO_TEST_SUITE_END()

View File

@ -16,6 +16,20 @@
namespace {
/** Takes the pre-computed and topologically-valid chunks and generates a fee diagram which starts at FeeFrac of (0, 0) */
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;
}
/** Evaluate a diagram at a specific size, returning the fee as a fraction.
*
* Fees in diagram cannot exceed 2^32, as the returned evaluation could overflow
@ -103,7 +117,7 @@ FUZZ_TARGET(build_and_compare_feerate_diagram)
assert(diagram1.front() == empty);
assert(diagram2.front() == empty);
auto real = CompareFeerateDiagram(diagram1, diagram2);
auto real = CompareChunks(chunks1, chunks2);
auto sim = CompareDiagrams(diagram1, diagram2);
assert(real == sim);

View File

@ -82,17 +82,6 @@ FUZZ_TARGET(rbf, .init = initialize_rbf)
}
}
void CheckDiagramConcave(std::vector<FeeFrac>& diagram)
{
// Diagrams are in monotonically-decreasing feerate order.
FeeFrac last_chunk = diagram.front();
for (size_t i = 1; i<diagram.size(); ++i) {
FeeFrac next_chunk = diagram[i] - diagram[i-1];
assert(next_chunk <= last_chunk);
last_chunk = next_chunk;
}
}
FUZZ_TARGET(package_rbf, .init = initialize_package_rbf)
{
FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
@ -107,7 +96,7 @@ FUZZ_TARGET(package_rbf, .init = initialize_package_rbf)
std::vector<CTransaction> mempool_txs;
size_t iter{0};
int64_t replacement_vsize = fuzzed_data_provider.ConsumeIntegralInRange<int64_t>(1, 1000000);
int32_t replacement_vsize = fuzzed_data_provider.ConsumeIntegralInRange<int32_t>(1, 1000000);
// Keep track of the total vsize of CTxMemPoolEntry's being added to the mempool to avoid overflow
// Add replacement_vsize since this is added to new diagram during RBF check
@ -167,32 +156,33 @@ FUZZ_TARGET(package_rbf, .init = initialize_package_rbf)
pool.CalculateDescendants(txiter, all_conflicts);
}
// Calculate the feerate diagrams for a replacement.
// Calculate the chunks for a replacement.
CAmount replacement_fees = ConsumeMoney(fuzzed_data_provider);
auto calc_results{pool.CalculateFeerateDiagramsForRBF(replacement_fees, replacement_vsize, direct_conflicts, all_conflicts)};
auto calc_results{pool.CalculateChunksForRBF(replacement_fees, replacement_vsize, direct_conflicts, all_conflicts)};
if (calc_results.has_value()) {
// Sanity checks on the diagrams.
// Sanity checks on the chunks.
// Diagrams start at 0.
assert(calc_results->first.front().size == 0);
assert(calc_results->first.front().fee == 0);
assert(calc_results->second.front().size == 0);
assert(calc_results->second.front().fee == 0);
CheckDiagramConcave(calc_results->first);
CheckDiagramConcave(calc_results->second);
CAmount replaced_fee{0};
int64_t replaced_size{0};
for (auto txiter : all_conflicts) {
replaced_fee += txiter->GetModifiedFee();
replaced_size += txiter->GetTxSize();
// Feerates are monotonically decreasing.
FeeFrac first_sum;
for (size_t i = 0; i < calc_results->first.size(); ++i) {
first_sum += calc_results->first[i];
if (i) assert(!(calc_results->first[i - 1] << calc_results->first[i]));
}
// The total fee of the new diagram should be the total fee of the old
// diagram - replaced_fee + replacement_fees
assert(calc_results->first.back().fee - replaced_fee + replacement_fees == calc_results->second.back().fee);
assert(calc_results->first.back().size - replaced_size + replacement_vsize == calc_results->second.back().size);
FeeFrac second_sum;
for (size_t i = 0; i < calc_results->second.size(); ++i) {
second_sum += calc_results->second[i];
if (i) assert(!(calc_results->second[i - 1] << calc_results->second[i]));
}
FeeFrac replaced;
for (auto txiter : all_conflicts) {
replaced.fee += txiter->GetModifiedFee();
replaced.size += txiter->GetTxSize();
}
// The total fee & size of the new diagram minus replaced fee & size should be the total
// fee & size of the old diagram minus replacement fee & size.
assert((first_sum - replaced) == (second_sum - FeeFrac{replacement_fees, replacement_vsize}));
}
// If internals report error, wrapper should too
@ -201,10 +191,12 @@ FUZZ_TARGET(package_rbf, .init = initialize_package_rbf)
assert(err_tuple.value().first == DiagramCheckError::UNCALCULABLE);
} else {
// Diagram check succeeded
auto old_sum = std::accumulate(calc_results->first.begin(), calc_results->first.end(), FeeFrac{});
auto new_sum = std::accumulate(calc_results->second.begin(), calc_results->second.end(), FeeFrac{});
if (!err_tuple.has_value()) {
// New diagram's final fee should always match or exceed old diagram's
assert(calc_results->first.back().fee <= calc_results->second.back().fee);
} else if (calc_results->first.back().fee > calc_results->second.back().fee) {
assert(old_sum.fee <= new_sum.fee);
} else if (old_sum.fee > new_sum.fee) {
// Or it failed, and if old diagram had higher fees, it should be a failure
assert(err_tuple.value().first == DiagramCheckError::FAILURE);
}

View File

@ -426,21 +426,21 @@ BOOST_FIXTURE_TEST_CASE(calc_feerate_diagram_rbf, TestChain100Setup)
// Replacement of size 1
{
const auto replace_one{pool.CalculateFeerateDiagramsForRBF(/*replacement_fees=*/0, /*replacement_vsize=*/1, {entry_low}, {entry_low})};
const auto replace_one{pool.CalculateChunksForRBF(/*replacement_fees=*/0, /*replacement_vsize=*/1, {entry_low}, {entry_low})};
BOOST_CHECK(replace_one.has_value());
std::vector<FeeFrac> expected_old_diagram{FeeFrac(0, 0), FeeFrac(low_fee, low_size)};
BOOST_CHECK(replace_one->first == expected_old_diagram);
std::vector<FeeFrac> expected_new_diagram{FeeFrac(0, 0), FeeFrac(0, 1)};
BOOST_CHECK(replace_one->second == expected_new_diagram);
std::vector<FeeFrac> expected_old_chunks{{low_fee, low_size}};
BOOST_CHECK(replace_one->first == expected_old_chunks);
std::vector<FeeFrac> expected_new_chunks{{0, 1}};
BOOST_CHECK(replace_one->second == expected_new_chunks);
}
// Non-zero replacement fee/size
{
const auto replace_one_fee{pool.CalculateFeerateDiagramsForRBF(/*replacement_fees=*/high_fee, /*replacement_vsize=*/low_size, {entry_low}, {entry_low})};
const auto replace_one_fee{pool.CalculateChunksForRBF(/*replacement_fees=*/high_fee, /*replacement_vsize=*/low_size, {entry_low}, {entry_low})};
BOOST_CHECK(replace_one_fee.has_value());
std::vector<FeeFrac> expected_old_diagram{FeeFrac(0, 0), FeeFrac(low_fee, low_size)};
std::vector<FeeFrac> expected_old_diagram{{low_fee, low_size}};
BOOST_CHECK(replace_one_fee->first == expected_old_diagram);
std::vector<FeeFrac> expected_new_diagram{FeeFrac(0, 0), FeeFrac(high_fee, low_size)};
std::vector<FeeFrac> expected_new_diagram{{high_fee, low_size}};
BOOST_CHECK(replace_one_fee->second == expected_new_diagram);
}
@ -451,22 +451,22 @@ BOOST_FIXTURE_TEST_CASE(calc_feerate_diagram_rbf, TestChain100Setup)
const auto high_size = entry_high->GetTxSize();
{
const auto replace_single_chunk{pool.CalculateFeerateDiagramsForRBF(/*replacement_fees=*/high_fee, /*replacement_vsize=*/low_size, {entry_low}, {entry_low, entry_high})};
const auto replace_single_chunk{pool.CalculateChunksForRBF(/*replacement_fees=*/high_fee, /*replacement_vsize=*/low_size, {entry_low}, {entry_low, entry_high})};
BOOST_CHECK(replace_single_chunk.has_value());
std::vector<FeeFrac> expected_old_diagram{FeeFrac(0, 0), FeeFrac(low_fee + high_fee, low_size + high_size)};
BOOST_CHECK(replace_single_chunk->first == expected_old_diagram);
std::vector<FeeFrac> expected_new_diagram{FeeFrac(0, 0), FeeFrac(high_fee, low_size)};
BOOST_CHECK(replace_single_chunk->second == expected_new_diagram);
std::vector<FeeFrac> expected_old_chunks{{low_fee + high_fee, low_size + high_size}};
BOOST_CHECK(replace_single_chunk->first == expected_old_chunks);
std::vector<FeeFrac> expected_new_chunks{{high_fee, low_size}};
BOOST_CHECK(replace_single_chunk->second == expected_new_chunks);
}
// Conflict with the 2nd tx, resulting in new diagram with three entries
{
const auto replace_cpfp_child{pool.CalculateFeerateDiagramsForRBF(/*replacement_fees=*/high_fee, /*replacement_vsize=*/low_size, {entry_high}, {entry_high})};
const auto replace_cpfp_child{pool.CalculateChunksForRBF(/*replacement_fees=*/high_fee, /*replacement_vsize=*/low_size, {entry_high}, {entry_high})};
BOOST_CHECK(replace_cpfp_child.has_value());
std::vector<FeeFrac> expected_old_diagram{FeeFrac(0, 0), FeeFrac(low_fee + high_fee, low_size + high_size)};
BOOST_CHECK(replace_cpfp_child->first == expected_old_diagram);
std::vector<FeeFrac> expected_new_diagram{FeeFrac(0, 0), FeeFrac(high_fee, low_size), FeeFrac(low_fee + high_fee, low_size + low_size)};
BOOST_CHECK(replace_cpfp_child->second == expected_new_diagram);
std::vector<FeeFrac> expected_old_chunks{{low_fee + high_fee, low_size + high_size}};
BOOST_CHECK(replace_cpfp_child->first == expected_old_chunks);
std::vector<FeeFrac> expected_new_chunks{{high_fee, low_size}, {low_fee, low_size}};
BOOST_CHECK(replace_cpfp_child->second == expected_new_chunks);
}
// third transaction causes the topology check to fail
@ -476,7 +476,7 @@ BOOST_FIXTURE_TEST_CASE(calc_feerate_diagram_rbf, TestChain100Setup)
const auto normal_size = entry_normal->GetTxSize();
{
const auto replace_too_large{pool.CalculateFeerateDiagramsForRBF(/*replacement_fees=*/normal_fee, /*replacement_vsize=*/normal_size, {entry_low}, {entry_low, entry_high, entry_normal})};
const auto replace_too_large{pool.CalculateChunksForRBF(/*replacement_fees=*/normal_fee, /*replacement_vsize=*/normal_size, {entry_low}, {entry_low, entry_high, entry_normal})};
BOOST_CHECK(!replace_too_large.has_value());
BOOST_CHECK_EQUAL(util::ErrorString(replace_too_large).original, strprintf("%s has 2 descendants, max 1 allowed", low_tx->GetHash().GetHex()));
}
@ -493,12 +493,12 @@ BOOST_FIXTURE_TEST_CASE(calc_feerate_diagram_rbf, TestChain100Setup)
const auto low_size_2 = entry_low_2->GetTxSize();
{
const auto replace_two_chunks_single_cluster{pool.CalculateFeerateDiagramsForRBF(/*replacement_fees=*/high_fee, /*replacement_vsize=*/low_size, {entry_high_2}, {entry_high_2, entry_low_2})};
const auto replace_two_chunks_single_cluster{pool.CalculateChunksForRBF(/*replacement_fees=*/high_fee, /*replacement_vsize=*/low_size, {entry_high_2}, {entry_high_2, entry_low_2})};
BOOST_CHECK(replace_two_chunks_single_cluster.has_value());
std::vector<FeeFrac> expected_old_diagram{FeeFrac(0, 0), FeeFrac(high_fee, high_size_2), FeeFrac(low_fee + high_fee, low_size_2 + high_size_2)};
BOOST_CHECK(replace_two_chunks_single_cluster->first == expected_old_diagram);
std::vector<FeeFrac> expected_new_diagram{FeeFrac(0, 0), FeeFrac(high_fee, low_size_2)};
BOOST_CHECK(replace_two_chunks_single_cluster->second == expected_new_diagram);
std::vector<FeeFrac> expected_old_chunks{{high_fee, high_size_2}, {low_fee, low_size_2}};
BOOST_CHECK(replace_two_chunks_single_cluster->first == expected_old_chunks);
std::vector<FeeFrac> expected_new_chunks{{high_fee, low_size_2}};
BOOST_CHECK(replace_two_chunks_single_cluster->second == expected_new_chunks);
}
// You can have more than two direct conflicts if the there are multiple affected clusters, all of size 2 or less
@ -515,10 +515,10 @@ BOOST_FIXTURE_TEST_CASE(calc_feerate_diagram_rbf, TestChain100Setup)
const auto conflict_3_entry = pool.GetIter(conflict_3->GetHash()).value();
{
const auto replace_multiple_clusters{pool.CalculateFeerateDiagramsForRBF(/*replacement_fees=*/high_fee, /*replacement_vsize=*/low_size, {conflict_1_entry, conflict_2_entry, conflict_3_entry}, {conflict_1_entry, conflict_2_entry, conflict_3_entry})};
const auto replace_multiple_clusters{pool.CalculateChunksForRBF(/*replacement_fees=*/high_fee, /*replacement_vsize=*/low_size, {conflict_1_entry, conflict_2_entry, conflict_3_entry}, {conflict_1_entry, conflict_2_entry, conflict_3_entry})};
BOOST_CHECK(replace_multiple_clusters.has_value());
BOOST_CHECK(replace_multiple_clusters->first.size() == 4);
BOOST_CHECK(replace_multiple_clusters->second.size() == 2);
BOOST_CHECK(replace_multiple_clusters->first.size() == 3);
BOOST_CHECK(replace_multiple_clusters->second.size() == 1);
}
// Add a child transaction to conflict_1 and make it cluster size 2, two chunks due to same feerate
@ -527,11 +527,11 @@ BOOST_FIXTURE_TEST_CASE(calc_feerate_diagram_rbf, TestChain100Setup)
const auto conflict_1_child_entry = pool.GetIter(conflict_1_child->GetHash()).value();
{
const auto replace_multiple_clusters_2{pool.CalculateFeerateDiagramsForRBF(/*replacement_fees=*/high_fee, /*replacement_vsize=*/low_size, {conflict_1_entry, conflict_2_entry, conflict_3_entry}, {conflict_1_entry, conflict_2_entry, conflict_3_entry, conflict_1_child_entry})};
const auto replace_multiple_clusters_2{pool.CalculateChunksForRBF(/*replacement_fees=*/high_fee, /*replacement_vsize=*/low_size, {conflict_1_entry, conflict_2_entry, conflict_3_entry}, {conflict_1_entry, conflict_2_entry, conflict_3_entry, conflict_1_child_entry})};
BOOST_CHECK(replace_multiple_clusters_2.has_value());
BOOST_CHECK(replace_multiple_clusters_2->first.size() == 5);
BOOST_CHECK(replace_multiple_clusters_2->second.size() == 2);
BOOST_CHECK(replace_multiple_clusters_2->first.size() == 4);
BOOST_CHECK(replace_multiple_clusters_2->second.size() == 1);
}
// Add another descendant to conflict_1, making the cluster size > 2 should fail at this point.
@ -540,86 +540,86 @@ BOOST_FIXTURE_TEST_CASE(calc_feerate_diagram_rbf, TestChain100Setup)
const auto conflict_1_grand_child_entry = pool.GetIter(conflict_1_child->GetHash()).value();
{
const auto replace_cluster_size_3{pool.CalculateFeerateDiagramsForRBF(/*replacement_fees=*/high_fee, /*replacement_vsize=*/low_size, {conflict_1_entry, conflict_2_entry, conflict_3_entry}, {conflict_1_entry, conflict_2_entry, conflict_3_entry, conflict_1_child_entry, conflict_1_grand_child_entry})};
const auto replace_cluster_size_3{pool.CalculateChunksForRBF(/*replacement_fees=*/high_fee, /*replacement_vsize=*/low_size, {conflict_1_entry, conflict_2_entry, conflict_3_entry}, {conflict_1_entry, conflict_2_entry, conflict_3_entry, conflict_1_child_entry, conflict_1_grand_child_entry})};
BOOST_CHECK(!replace_cluster_size_3.has_value());
BOOST_CHECK_EQUAL(util::ErrorString(replace_cluster_size_3).original, strprintf("%s has 2 descendants, max 1 allowed", conflict_1->GetHash().GetHex()));
}
}
BOOST_AUTO_TEST_CASE(feerate_diagram_utilities)
BOOST_AUTO_TEST_CASE(feerate_chunks_utilities)
{
// Sanity check the correctness of the feerate diagram comparison.
// Sanity check the correctness of the feerate chunks comparison.
// A strictly better case.
std::vector<FeeFrac> old_diagram{{FeeFrac{0, 0}, FeeFrac{950, 300}, FeeFrac{1050, 400}}};
std::vector<FeeFrac> new_diagram{{FeeFrac{0, 0}, FeeFrac{1000, 300}, FeeFrac{1050, 400}}};
std::vector<FeeFrac> old_chunks{{{950, 300}, {100, 100}}};
std::vector<FeeFrac> new_chunks{{{1000, 300}, {50, 100}}};
BOOST_CHECK(std::is_lt(CompareFeerateDiagram(old_diagram, new_diagram)));
BOOST_CHECK(std::is_gt(CompareFeerateDiagram(new_diagram, old_diagram)));
BOOST_CHECK(std::is_lt(CompareChunks(old_chunks, new_chunks)));
BOOST_CHECK(std::is_gt(CompareChunks(new_chunks, old_chunks)));
// Incomparable diagrams
old_diagram = {FeeFrac{0, 0}, FeeFrac{950, 300}, FeeFrac{1050, 400}};
new_diagram = {FeeFrac{0, 0}, FeeFrac{1000, 300}, FeeFrac{1000, 400}};
old_chunks = {{950, 300}, {100, 100}};
new_chunks = {{1000, 300}, {0, 100}};
BOOST_CHECK(CompareFeerateDiagram(old_diagram, new_diagram) == std::partial_ordering::unordered);
BOOST_CHECK(CompareFeerateDiagram(new_diagram, old_diagram) == std::partial_ordering::unordered);
BOOST_CHECK(CompareChunks(old_chunks, new_chunks) == std::partial_ordering::unordered);
BOOST_CHECK(CompareChunks(new_chunks, old_chunks) == std::partial_ordering::unordered);
// Strictly better but smaller size.
old_diagram = {FeeFrac{0, 0}, FeeFrac{950, 300}, FeeFrac{1050, 400}};
new_diagram = {FeeFrac{0, 0}, FeeFrac{1100, 300}};
old_chunks = {{950, 300}, {100, 100}};
new_chunks = {{1100, 300}};
BOOST_CHECK(std::is_lt(CompareFeerateDiagram(old_diagram, new_diagram)));
BOOST_CHECK(std::is_gt(CompareFeerateDiagram(new_diagram, old_diagram)));
BOOST_CHECK(std::is_lt(CompareChunks(old_chunks, new_chunks)));
BOOST_CHECK(std::is_gt(CompareChunks(new_chunks, old_chunks)));
// New diagram is strictly better due to the first chunk, even though
// second chunk contributes no fees
old_diagram = {FeeFrac{0, 0}, FeeFrac{950, 300}, FeeFrac{1050, 400}};
new_diagram = {FeeFrac{0, 0}, FeeFrac{1100, 100}, FeeFrac{1100, 200}};
old_chunks = {{950, 300}, {100, 100}};
new_chunks = {{1100, 100}, {0, 100}};
BOOST_CHECK(std::is_lt(CompareFeerateDiagram(old_diagram, new_diagram)));
BOOST_CHECK(std::is_gt(CompareFeerateDiagram(new_diagram, old_diagram)));
BOOST_CHECK(std::is_lt(CompareChunks(old_chunks, new_chunks)));
BOOST_CHECK(std::is_gt(CompareChunks(new_chunks, old_chunks)));
// Feerate of first new chunk is better with, but second chunk is worse
old_diagram = {FeeFrac{0, 0}, FeeFrac{950, 300}, FeeFrac{1050, 400}};
new_diagram = {FeeFrac{0, 0}, FeeFrac{750, 100}, FeeFrac{999, 350}, FeeFrac{1150, 1000}};
old_chunks = {{950, 300}, {100, 100}};
new_chunks = {{750, 100}, {249, 250}, {151, 650}};
BOOST_CHECK(CompareFeerateDiagram(old_diagram, new_diagram) == std::partial_ordering::unordered);
BOOST_CHECK(CompareFeerateDiagram(new_diagram, old_diagram) == std::partial_ordering::unordered);
BOOST_CHECK(CompareChunks(old_chunks, new_chunks) == std::partial_ordering::unordered);
BOOST_CHECK(CompareChunks(new_chunks, old_chunks) == std::partial_ordering::unordered);
// If we make the second chunk slightly better, the new diagram now wins.
old_diagram = {FeeFrac{0, 0}, FeeFrac{950, 300}, FeeFrac{1050, 400}};
new_diagram = {FeeFrac{0, 0}, FeeFrac{750, 100}, FeeFrac{1000, 350}, FeeFrac{1150, 500}};
old_chunks = {{950, 300}, {100, 100}};
new_chunks = {{750, 100}, {250, 250}, {150, 150}};
BOOST_CHECK(std::is_lt(CompareFeerateDiagram(old_diagram, new_diagram)));
BOOST_CHECK(std::is_gt(CompareFeerateDiagram(new_diagram, old_diagram)));
BOOST_CHECK(std::is_lt(CompareChunks(old_chunks, new_chunks)));
BOOST_CHECK(std::is_gt(CompareChunks(new_chunks, old_chunks)));
// Identical diagrams, cannot be strictly better
old_diagram = {FeeFrac{0, 0}, FeeFrac{950, 300}, FeeFrac{1050, 400}};
new_diagram = {FeeFrac{0, 0}, FeeFrac{950, 300}, FeeFrac{1050, 400}};
old_chunks = {{950, 300}, {100, 100}};
new_chunks = {{950, 300}, {100, 100}};
BOOST_CHECK(std::is_eq(CompareFeerateDiagram(old_diagram, new_diagram)));
BOOST_CHECK(std::is_eq(CompareFeerateDiagram(new_diagram, old_diagram)));
BOOST_CHECK(std::is_eq(CompareChunks(old_chunks, new_chunks)));
BOOST_CHECK(std::is_eq(CompareChunks(new_chunks, old_chunks)));
// Same aggregate fee, but different total size (trigger single tail fee check step)
old_diagram = {FeeFrac{0, 0}, FeeFrac{950, 300}, FeeFrac{1050, 399}};
new_diagram = {FeeFrac{0, 0}, FeeFrac{950, 300}, FeeFrac{1050, 400}};
old_chunks = {{950, 300}, {100, 99}};
new_chunks = {{950, 300}, {100, 100}};
// No change in evaluation when tail check needed.
BOOST_CHECK(std::is_gt(CompareFeerateDiagram(old_diagram, new_diagram)));
BOOST_CHECK(std::is_lt(CompareFeerateDiagram(new_diagram, old_diagram)));
BOOST_CHECK(std::is_gt(CompareChunks(old_chunks, new_chunks)));
BOOST_CHECK(std::is_lt(CompareChunks(new_chunks, old_chunks)));
// Trigger multiple tail fee check steps
old_diagram = {FeeFrac{0, 0}, FeeFrac{950, 300}, FeeFrac{1050, 399}};
new_diagram = {FeeFrac{0, 0}, FeeFrac{950, 300}, FeeFrac{1050, 400}, FeeFrac{1050, 401}, FeeFrac{1050, 402}};
old_chunks = {{950, 300}, {100, 99}};
new_chunks = {{950, 300}, {100, 100}, {0, 1}, {0, 1}};
BOOST_CHECK(std::is_gt(CompareFeerateDiagram(old_diagram, new_diagram)));
BOOST_CHECK(std::is_lt(CompareFeerateDiagram(new_diagram, old_diagram)));
BOOST_CHECK(std::is_gt(CompareChunks(old_chunks, new_chunks)));
BOOST_CHECK(std::is_lt(CompareChunks(new_chunks, old_chunks)));
// Multiple tail fee check steps, unordered result
new_diagram = {FeeFrac{0, 0}, FeeFrac{950, 300}, FeeFrac{1050, 400}, FeeFrac{1050, 401}, FeeFrac{1050, 402}, FeeFrac{1051, 403}};
BOOST_CHECK(CompareFeerateDiagram(old_diagram, new_diagram) == std::partial_ordering::unordered);
BOOST_CHECK(CompareFeerateDiagram(new_diagram, old_diagram) == std::partial_ordering::unordered);
new_chunks = {{950, 300}, {100, 100}, {0, 1}, {0, 1}, {1, 1}};
BOOST_CHECK(CompareChunks(old_chunks, new_chunks) == std::partial_ordering::unordered);
BOOST_CHECK(CompareChunks(new_chunks, old_chunks) == std::partial_ordering::unordered);
}
BOOST_AUTO_TEST_SUITE_END()

View File

@ -1280,7 +1280,7 @@ std::optional<std::string> CTxMemPool::CheckConflictTopology(const setEntries& d
return std::nullopt;
}
util::Result<std::pair<std::vector<FeeFrac>, std::vector<FeeFrac>>> CTxMemPool::CalculateFeerateDiagramsForRBF(CAmount replacement_fees, int64_t replacement_vsize, const setEntries& direct_conflicts, const setEntries& all_conflicts)
util::Result<std::pair<std::vector<FeeFrac>, std::vector<FeeFrac>>> CTxMemPool::CalculateChunksForRBF(CAmount replacement_fees, int64_t replacement_vsize, const setEntries& direct_conflicts, const setEntries& all_conflicts)
{
Assume(replacement_vsize > 0);
@ -1335,7 +1335,6 @@ util::Result<std::pair<std::vector<FeeFrac>, std::vector<FeeFrac>>> CTxMemPool::
// No topology restrictions post-chunking; sort
std::sort(old_chunks.begin(), old_chunks.end(), std::greater());
std::vector<FeeFrac> old_diagram = BuildDiagramFromChunks(old_chunks);
std::vector<FeeFrac> new_chunks;
@ -1365,6 +1364,5 @@ util::Result<std::pair<std::vector<FeeFrac>, std::vector<FeeFrac>>> CTxMemPool::
// No topology restrictions post-chunking; sort
std::sort(new_chunks.begin(), new_chunks.end(), std::greater());
std::vector<FeeFrac> new_diagram = BuildDiagramFromChunks(new_chunks);
return std::make_pair(old_diagram, new_diagram);
return std::make_pair(old_chunks, new_chunks);
}

View File

@ -738,7 +738,7 @@ public:
}
/**
* Calculate the old and new mempool feerate diagrams relating to the
* Calculate the sorted chunks for the old and new mempool relating to the
* clusters that would be affected by a potential replacement transaction.
* (replacement_fees, replacement_vsize) values are gathered from a
* proposed set of replacement transactions that are considered as a single
@ -752,7 +752,7 @@ public:
* @param[in] all_conflicts All transactions that would be removed
* @return old and new diagram pair respectively, or an error string if the conflicts don't match a calculable topology
*/
util::Result<std::pair<std::vector<FeeFrac>, std::vector<FeeFrac>>> CalculateFeerateDiagramsForRBF(CAmount replacement_fees, int64_t replacement_vsize, const setEntries& direct_conflicts, const setEntries& all_conflicts) EXCLUSIVE_LOCKS_REQUIRED(cs);
util::Result<std::pair<std::vector<FeeFrac>, std::vector<FeeFrac>>> CalculateChunksForRBF(CAmount replacement_fees, int64_t replacement_vsize, const setEntries& direct_conflicts, const setEntries& all_conflicts) EXCLUSIVE_LOCKS_REQUIRED(cs);
/* Check that all direct conflicts are in a cluster size of two or less. Each
* direct conflict may be in a separate cluster.

View File

@ -7,39 +7,26 @@
#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)
std::partial_ordering CompareChunks(Span<const FeeFrac> chunks0, Span<const FeeFrac> chunks1)
{
/** Array to allow indexed access to input diagrams. */
const std::array<Span<const FeeFrac>, 2> dias = {dia0, dia1};
const std::array<Span<const FeeFrac>, 2> chunk = {chunks0, chunks1};
/** How many elements we have processed in each input. */
size_t next_index[2] = {1, 1};
size_t next_index[2] = {0, 0};
/** Accumulated fee/sizes in diagrams, up to next_index[i] - 1. */
FeeFrac accum[2];
/** 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]]; };
const auto next_point = [&](int dia) { return chunk[dia][next_index[dia]] + accum[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());
const auto prev_point = [&](int dia) { return accum[dia]; };
/** Move to the next point in diagram number dia. */
const auto advance = [&](int dia) { accum[dia] += chunk[dia][next_index[dia]++]; };
do {
bool done_0 = next_index[0] == dias[0].size();
bool done_1 = next_index[1] == dias[1].size();
bool done_0 = next_index[0] == chunk[0].size();
bool done_1 = next_index[1] == chunk[1].size();
if (done_0 && done_1) break;
// Determine which diagram has the first unprocessed point. If a single side is finished, use the
@ -69,17 +56,16 @@ std::partial_ordering CompareFeerateDiagram(Span<const FeeFrac> dia0, Span<const
// 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 (point_b.size == point_p.size) advance(!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];
advance(unproc_side);
// If both diagrams are better somewhere, they are incomparable.
if (better_somewhere[0] && better_somewhere[1]) return std::partial_ordering::unordered;
} while(true);
// Otherwise compare the better_somewhere values.

View File

@ -146,15 +146,14 @@ struct FeeFrac
}
};
/** 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.
/** Compare the feerate diagrams implied by the provided sorted chunks data.
*
* 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).
* The implied diagram for each starts at (0, 0), then contains for each chunk the cumulative fee
* and size up to that chunk, and then extends infinitely to the right with a horizontal line.
*
* The caller must guarantee that the sum of the FeeFracs in either of the chunks' data set do not
* overflow (so sum fees < 2^63, and sum sizes < 2^31).
*/
std::partial_ordering CompareFeerateDiagram(Span<const FeeFrac> dia0, Span<const FeeFrac> dia1);
std::partial_ordering CompareChunks(Span<const FeeFrac> chunks0, Span<const FeeFrac> chunks1);
#endif // BITCOIN_UTIL_FEEFRAC_H