@@ -334,6 +334,164 @@ BOOST_FIXTURE_TEST_CASE(improves_feerate, TestChain100Setup)
334334 BOOST_CHECK (res3.has_value ());
335335 BOOST_CHECK (res3.value ().first == DiagramCheckError::UNCALCULABLE);
336336 BOOST_CHECK (res3.value ().second == strprintf (" %s has 2 descendants, max 1 allowed" , tx1->GetHash ().GetHex ()));
337+
338+ }
339+
340+ BOOST_FIXTURE_TEST_CASE (calc_feerate_diagram_rbf, TestChain100Setup)
341+ {
342+ CTxMemPool& pool = *Assert (m_node.mempool );
343+ LOCK2 (::cs_main, pool.cs );
344+ TestMemPoolEntryHelper entry;
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
351+ const auto low_tx = make_tx (/* inputs=*/ {m_coinbase_txns[0 ]}, /* output_values=*/ {10 * COIN});
352+ pool.addUnchecked (entry.Fee (low_fee).FromTx (low_tx));
353+
354+ const auto entry_low = pool.GetIter (low_tx->GetHash ()).value ();
355+ const auto low_size = entry_low->GetTxSize ();
356+
357+ std::vector<FeeFrac> old_diagram, new_diagram;
358+
359+ // Replacement of size 1
360+ const auto replace_one{pool.CalculateFeerateDiagramsForRBF (/* replacement_fees=*/ 0 , /* replacement_vsize=*/ 1 , {entry_low}, {entry_low})};
361+ BOOST_CHECK (replace_one.has_value ());
362+ old_diagram = replace_one->first ;
363+ new_diagram = replace_one->second ;
364+ BOOST_CHECK (old_diagram.size () == 2 );
365+ BOOST_CHECK (new_diagram.size () == 2 );
366+ BOOST_CHECK (old_diagram[0 ] == FeeFrac (0 , 0 ));
367+ BOOST_CHECK (old_diagram[1 ] == FeeFrac (low_fee, low_size));
368+ BOOST_CHECK (new_diagram[0 ] == FeeFrac (0 , 0 ));
369+ BOOST_CHECK (new_diagram[1 ] == FeeFrac (0 , 1 ));
370+
371+ // Non-zero replacement fee/size
372+ const auto replace_one_fee{pool.CalculateFeerateDiagramsForRBF (/* replacement_fees=*/ high_fee, /* replacement_vsize=*/ low_size, {entry_low}, {entry_low})};
373+ BOOST_CHECK (replace_one_fee.has_value ());
374+ old_diagram = replace_one_fee->first ;
375+ new_diagram = replace_one_fee->second ;
376+ BOOST_CHECK (old_diagram.size () == 2 );
377+ BOOST_CHECK (new_diagram.size () == 2 );
378+ BOOST_CHECK (old_diagram[0 ] == FeeFrac (0 , 0 ));
379+ BOOST_CHECK (old_diagram[1 ] == FeeFrac (low_fee, low_size));
380+ BOOST_CHECK (new_diagram[0 ] == FeeFrac (0 , 0 ));
381+ BOOST_CHECK (new_diagram[1 ] == FeeFrac (high_fee, low_size));
382+
383+ // Add a second transaction to the cluster that will make a single chunk, to be evicted in the RBF
384+ const auto high_tx = make_tx (/* inputs=*/ {low_tx}, /* output_values=*/ {995 * CENT});
385+ pool.addUnchecked (entry.Fee (high_fee).FromTx (high_tx));
386+ const auto entry_high = pool.GetIter (high_tx->GetHash ()).value ();
387+ const auto high_size = entry_high->GetTxSize ();
388+
389+ const auto replace_single_chunk{pool.CalculateFeerateDiagramsForRBF (/* replacement_fees=*/ high_fee, /* replacement_vsize=*/ low_size, {entry_low}, {entry_low, entry_high})};
390+ BOOST_CHECK (replace_single_chunk.has_value ());
391+ old_diagram = replace_single_chunk->first ;
392+ new_diagram = replace_single_chunk->second ;
393+ BOOST_CHECK (old_diagram.size () == 2 );
394+ BOOST_CHECK (new_diagram.size () == 2 );
395+ BOOST_CHECK (old_diagram[0 ] == FeeFrac (0 , 0 ));
396+ BOOST_CHECK (old_diagram[1 ] == FeeFrac (low_fee + high_fee, low_size + high_size));
397+ BOOST_CHECK (new_diagram[0 ] == FeeFrac (0 , 0 ));
398+ BOOST_CHECK (new_diagram[1 ] == FeeFrac (high_fee, low_size));
399+
400+ // Conflict with the 2nd tx, resulting in new diagram with three entries
401+ const auto replace_cpfp_child{pool.CalculateFeerateDiagramsForRBF (/* replacement_fees=*/ high_fee, /* replacement_vsize=*/ low_size, {entry_high}, {entry_high})};
402+ BOOST_CHECK (replace_cpfp_child.has_value ());
403+ old_diagram = replace_cpfp_child->first ;
404+ new_diagram = replace_cpfp_child->second ;
405+ BOOST_CHECK (old_diagram.size () == 2 );
406+ BOOST_CHECK (new_diagram.size () == 3 );
407+ BOOST_CHECK (old_diagram[0 ] == FeeFrac (0 , 0 ));
408+ BOOST_CHECK (old_diagram[1 ] == FeeFrac (low_fee + high_fee, low_size + high_size));
409+ BOOST_CHECK (new_diagram[0 ] == FeeFrac (0 , 0 ));
410+ BOOST_CHECK (new_diagram[1 ] == FeeFrac (high_fee, low_size));
411+ BOOST_CHECK (new_diagram[2 ] == FeeFrac (low_fee + high_fee, low_size + low_size));
412+
413+ // third transaction causes the topology check to fail
414+ const auto normal_tx = make_tx (/* inputs=*/ {high_tx}, /* output_values=*/ {995 * CENT});
415+ pool.addUnchecked (entry.Fee (normal_fee).FromTx (normal_tx));
416+ const auto entry_normal = pool.GetIter (normal_tx->GetHash ()).value ();
417+ const auto normal_size = entry_normal->GetTxSize ();
418+
419+ const auto replace_too_large{pool.CalculateFeerateDiagramsForRBF (/* replacement_fees=*/ normal_fee, /* replacement_vsize=*/ normal_size, {entry_low}, {entry_low, entry_high, entry_normal})};
420+ BOOST_CHECK (!replace_too_large.has_value ());
421+ BOOST_CHECK_EQUAL (util::ErrorString (replace_too_large).original , strprintf (" %s has 2 descendants, max 1 allowed" , low_tx->GetHash ().GetHex ()));
422+ old_diagram.clear ();
423+ new_diagram.clear ();
424+
425+ // Make a size 2 cluster that is itself two chunks; evict both txns
426+ const auto high_tx_2 = make_tx (/* inputs=*/ {m_coinbase_txns[1 ]}, /* output_values=*/ {10 * COIN});
427+ pool.addUnchecked (entry.Fee (high_fee).FromTx (high_tx_2));
428+ const auto entry_high_2 = pool.GetIter (high_tx_2->GetHash ()).value ();
429+ const auto high_size_2 = entry_high_2->GetTxSize ();
430+
431+ const auto low_tx_2 = make_tx (/* inputs=*/ {high_tx_2}, /* output_values=*/ {9 * COIN});
432+ pool.addUnchecked (entry.Fee (low_fee).FromTx (low_tx_2));
433+ const auto entry_low_2 = pool.GetIter (low_tx_2->GetHash ()).value ();
434+ const auto low_size_2 = entry_low_2->GetTxSize ();
435+
436+ 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})};
437+ BOOST_CHECK (replace_two_chunks_single_cluster.has_value ());
438+ old_diagram = replace_two_chunks_single_cluster->first ;
439+ new_diagram = replace_two_chunks_single_cluster->second ;
440+ BOOST_CHECK (old_diagram.size () == 3 );
441+ BOOST_CHECK (new_diagram.size () == 2 );
442+ BOOST_CHECK (old_diagram[0 ] == FeeFrac (0 , 0 ));
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
449+ const auto conflict_1 = make_tx (/* inputs=*/ {m_coinbase_txns[2 ]}, /* output_values=*/ {10 * COIN});
450+ pool.addUnchecked (entry.Fee (low_fee).FromTx (conflict_1));
451+ const auto conflict_1_entry = pool.GetIter (conflict_1->GetHash ()).value ();
452+
453+ const auto conflict_2 = make_tx (/* inputs=*/ {m_coinbase_txns[3 ]}, /* output_values=*/ {10 * COIN});
454+ pool.addUnchecked (entry.Fee (low_fee).FromTx (conflict_2));
455+ const auto conflict_2_entry = pool.GetIter (conflict_2->GetHash ()).value ();
456+
457+ const auto conflict_3 = make_tx (/* inputs=*/ {m_coinbase_txns[4 ]}, /* output_values=*/ {10 * COIN});
458+ pool.addUnchecked (entry.Fee (low_fee).FromTx (conflict_3));
459+ const auto conflict_3_entry = pool.GetIter (conflict_3->GetHash ()).value ();
460+
461+ 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})};
462+
463+ BOOST_CHECK (replace_multiple_clusters.has_value ());
464+ old_diagram = replace_multiple_clusters->first ;
465+ new_diagram = replace_multiple_clusters->second ;
466+ BOOST_CHECK (old_diagram.size () == 4 );
467+ BOOST_CHECK (new_diagram.size () == 2 );
468+
469+ // Add a child transaction to conflict_1 and make it cluster size 2, still one chunk due to same feerate
470+ const auto conflict_1_child = make_tx (/* inputs=*/ {conflict_1}, /* output_values=*/ {995 * CENT});
471+ pool.addUnchecked (entry.Fee (low_fee).FromTx (conflict_1_child));
472+ const auto conflict_1_child_entry = pool.GetIter (conflict_1_child->GetHash ()).value ();
473+
474+ 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})};
475+
476+ BOOST_CHECK (replace_multiple_clusters_2.has_value ());
477+ old_diagram = replace_multiple_clusters_2->first ;
478+ new_diagram = replace_multiple_clusters_2->second ;
479+ BOOST_CHECK (old_diagram.size () == 4 );
480+ BOOST_CHECK (new_diagram.size () == 2 );
481+ old_diagram.clear ();
482+ new_diagram.clear ();
483+
484+ // Add another descendant to conflict_1, making the cluster size > 2 should fail at this point.
485+ const auto conflict_1_grand_child = make_tx (/* inputs=*/ {conflict_1_child}, /* output_values=*/ {995 * CENT});
486+ pool.addUnchecked (entry.Fee (high_fee).FromTx (conflict_1_grand_child));
487+ const auto conflict_1_grand_child_entry = pool.GetIter (conflict_1_child->GetHash ()).value ();
488+
489+ 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})};
490+
491+ BOOST_CHECK (!replace_cluster_size_3.has_value ());
492+ BOOST_CHECK_EQUAL (util::ErrorString (replace_cluster_size_3).original , strprintf (" %s has 2 descendants, max 1 allowed" , conflict_1->GetHash ().GetHex ()));
493+ BOOST_CHECK (old_diagram.empty ());
494+ BOOST_CHECK (new_diagram.empty ());
337495}
338496
339497BOOST_AUTO_TEST_CASE (feerate_diagram_utilities)
0 commit comments