Skip to content

Commit 2e35e94

Browse files
committed
Bump unconfirmed parent txs to target feerate
When a transaction uses an unconfirmed input, preceding this commit it would not consider the feerate of the parent transaction. Given a parent transaction with a lower ancestor feerate, this resulted in the new transaction's ancestor feerate undershooting the target feerate. This commit changes how we calculate the effective value of unconfirmed UTXOs. The effective value of unconfirmed UTXOs is decreased by the fee necessary to bump its ancestry to the target feerate. This also impacts the calculation of the waste metric: since the estimate for the current fee is increased by the bump fees, unconfirmed UTXOs current fees appear less favorable compared to their unchanged long term fees. This has one caveat: if multiple UTXOs have overlapping ancestries, each of their individual estimates will account for bumping all ancestors.
1 parent 3e3e052 commit 2e35e94

File tree

7 files changed

+588
-6
lines changed

7 files changed

+588
-6
lines changed

src/wallet/coinselection.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,12 @@ CAmount SelectionResult::GetSelectedEffectiveValue() const
514514
return std::accumulate(m_selected_inputs.cbegin(), m_selected_inputs.cend(), CAmount{0}, [](CAmount sum, const auto& coin) { return sum + coin->GetEffectiveValue(); });
515515
}
516516

517+
518+
CAmount SelectionResult::GetTotalBumpFees() const
519+
{
520+
return std::accumulate(m_selected_inputs.cbegin(), m_selected_inputs.cend(), CAmount{0}, [](CAmount sum, const auto& coin) { return sum + coin->ancestor_bump_fees; });
521+
}
522+
517523
void SelectionResult::Clear()
518524
{
519525
m_selected_inputs.clear();

src/wallet/coinselection.h

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
#include <optional>
1919

20+
2021
namespace wallet {
2122
//! lower bound for randomly-chosen target change amount
2223
static constexpr CAmount CHANGE_LOWER{50000};
@@ -26,10 +27,10 @@ static constexpr CAmount CHANGE_UPPER{1000000};
2627
/** A UTXO under consideration for use in funding a new transaction. */
2728
struct COutput {
2829
private:
29-
/** The output's value minus fees required to spend it.*/
30+
/** The output's value minus fees required to spend it and bump its unconfirmed ancestors to the target feerate. */
3031
std::optional<CAmount> effective_value;
3132

32-
/** The fee required to spend this output at the transaction's target feerate. */
33+
/** The fee required to spend this output at the transaction's target feerate and to bump its unconfirmed ancestors to the target feerate. */
3334
std::optional<CAmount> fee;
3435

3536
public:
@@ -71,6 +72,9 @@ struct COutput {
7172
/** The fee required to spend this output at the consolidation feerate. */
7273
CAmount long_term_fee{0};
7374

75+
/** The fee necessary to bump this UTXO's ancestor transactions to the target feerate */
76+
CAmount ancestor_bump_fees{0};
77+
7478
COutput(const COutPoint& outpoint, const CTxOut& txout, int depth, int input_bytes, bool spendable, bool solvable, bool safe, int64_t time, bool from_me, const std::optional<CFeeRate> feerate = std::nullopt)
7579
: outpoint{outpoint},
7680
txout{txout},
@@ -83,6 +87,7 @@ struct COutput {
8387
from_me{from_me}
8488
{
8589
if (feerate) {
90+
// base fee without considering potential unconfirmed ancestors
8691
fee = input_bytes < 0 ? 0 : feerate.value().GetFee(input_bytes);
8792
effective_value = txout.nValue - fee.value();
8893
}
@@ -104,6 +109,16 @@ struct COutput {
104109
return outpoint < rhs.outpoint;
105110
}
106111

112+
void ApplyBumpFee(CAmount bump_fee)
113+
{
114+
assert(bump_fee >= 0);
115+
ancestor_bump_fees = bump_fee;
116+
assert(fee);
117+
*fee += bump_fee;
118+
// Note: assert(effective_value - bump_fee == nValue - fee.value());
119+
effective_value = txout.nValue - fee.value();
120+
}
121+
107122
CAmount GetFee() const
108123
{
109124
assert(fee.has_value());
@@ -355,6 +370,8 @@ struct SelectionResult
355370

356371
[[nodiscard]] CAmount GetSelectedEffectiveValue() const;
357372

373+
[[nodiscard]] CAmount GetTotalBumpFees() const;
374+
358375
void Clear();
359376

360377
void AddInput(const OutputGroup& group);

src/wallet/feebumper.cpp

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ static feebumper::Result PreconditionChecks(const CWallet& wallet, const CWallet
6363
}
6464

6565
//! Check if the user provided a valid feeRate
66-
static feebumper::Result CheckFeeRate(const CWallet& wallet, const CFeeRate& newFeerate, const int64_t maxTxSize, CAmount old_fee, std::vector<bilingual_str>& errors)
66+
static feebumper::Result CheckFeeRate(const CWallet& wallet, const CMutableTransaction& mtx, const CFeeRate& newFeerate, const int64_t maxTxSize, CAmount old_fee, std::vector<bilingual_str>& errors)
6767
{
6868
// check that fee rate is higher than mempool's minimum fee
6969
// (no point in bumping fee if we know that the new tx won't be accepted to the mempool)
@@ -80,7 +80,19 @@ static feebumper::Result CheckFeeRate(const CWallet& wallet, const CFeeRate& new
8080
return feebumper::Result::WALLET_ERROR;
8181
}
8282

83-
CAmount new_total_fee = newFeerate.GetFee(maxTxSize);
83+
std::vector<COutPoint> reused_inputs;
84+
reused_inputs.reserve(mtx.vin.size());
85+
for (const CTxIn& txin : mtx.vin) {
86+
reused_inputs.push_back(txin.prevout);
87+
}
88+
89+
std::map<COutPoint, CAmount> bump_fees = wallet.chain().CalculateIndividualBumpFees(reused_inputs, newFeerate);
90+
CAmount total_bump_fees = 0;
91+
for (auto& [_, bump_fee] : bump_fees) {
92+
total_bump_fees += bump_fee;
93+
}
94+
95+
CAmount new_total_fee = newFeerate.GetFee(maxTxSize) + total_bump_fees;
8496

8597
CFeeRate incrementalRelayFee = std::max(wallet.chain().relayIncrementalFee(), CFeeRate(WALLET_INCREMENTAL_RELAY_FEE));
8698

@@ -283,7 +295,7 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo
283295
}
284296
temp_mtx.vout = txouts;
285297
const int64_t maxTxSize{CalculateMaximumSignedTxSize(CTransaction(temp_mtx), &wallet, &new_coin_control).vsize};
286-
Result res = CheckFeeRate(wallet, *new_coin_control.m_feerate, maxTxSize, old_fee, errors);
298+
Result res = CheckFeeRate(wallet, temp_mtx, *new_coin_control.m_feerate, maxTxSize, old_fee, errors);
287299
if (res != Result::OK) {
288300
return res;
289301
}

src/wallet/spend.cpp

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ util::Result<PreSelectedInputs> FetchSelectedInputs(const CWallet& wallet, const
162162
{
163163
PreSelectedInputs result;
164164
const bool can_grind_r = wallet.CanGrindR();
165+
std::map<COutPoint, CAmount> map_of_bump_fees = wallet.chain().CalculateIndividualBumpFees(coin_control.ListSelected(), coin_selection_params.m_effective_feerate);
165166
for (const COutPoint& outpoint : coin_control.ListSelected()) {
166167
int input_bytes = -1;
167168
CTxOut txout;
@@ -197,6 +198,7 @@ util::Result<PreSelectedInputs> FetchSelectedInputs(const CWallet& wallet, const
197198

198199
/* Set some defaults for depth, spendable, solvable, safe, time, and from_me as these don't matter for preset inputs since no selection is being done. */
199200
COutput output(outpoint, txout, /*depth=*/ 0, input_bytes, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false, coin_selection_params.m_effective_feerate);
201+
output.ApplyBumpFee(map_of_bump_fees.at(output.outpoint));
200202
result.Insert(output, coin_selection_params.m_subtract_fee_outputs);
201203
}
202204
return result;
@@ -217,6 +219,7 @@ CoinsResult AvailableCoins(const CWallet& wallet,
217219
const int max_depth = {coinControl ? coinControl->m_max_depth : DEFAULT_MAX_DEPTH};
218220
const bool only_safe = {coinControl ? !coinControl->m_include_unsafe_inputs : true};
219221
const bool can_grind_r = wallet.CanGrindR();
222+
std::vector<COutPoint> outpoints;
220223

221224
std::set<uint256> trusted_parents;
222225
for (const auto& entry : wallet.mapWallet)
@@ -334,6 +337,8 @@ CoinsResult AvailableCoins(const CWallet& wallet,
334337
result.Add(GetOutputType(type, is_from_p2sh),
335338
COutput(outpoint, output, nDepth, input_bytes, spendable, solvable, safeTx, wtx.GetTxTime(), tx_from_me, feerate));
336339

340+
outpoints.push_back(outpoint);
341+
337342
// Checks the sum amount of all UTXO's.
338343
if (params.min_sum_amount != MAX_MONEY) {
339344
if (result.GetTotalAmount() >= params.min_sum_amount) {
@@ -348,6 +353,16 @@ CoinsResult AvailableCoins(const CWallet& wallet,
348353
}
349354
}
350355

356+
if (feerate.has_value()) {
357+
std::map<COutPoint, CAmount> map_of_bump_fees = wallet.chain().CalculateIndividualBumpFees(outpoints, feerate.value());
358+
359+
for (auto& [_, outputs] : result.coins) {
360+
for (auto& output : outputs) {
361+
output.ApplyBumpFee(map_of_bump_fees.at(output.outpoint));
362+
}
363+
}
364+
}
365+
351366
return result;
352367
}
353368

@@ -1021,7 +1036,7 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
10211036
if (nBytes == -1) {
10221037
return util::Error{_("Missing solving data for estimating transaction size")};
10231038
}
1024-
CAmount fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes);
1039+
CAmount fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes) + result.GetTotalBumpFees();
10251040
const CAmount output_value = CalculateOutputValue(txNew);
10261041
Assume(recipients_sum + change_amount == output_value);
10271042
CAmount current_fee = result.GetSelectedValue() - output_value;

src/wallet/test/coinselector_tests.cpp

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -954,6 +954,53 @@ BOOST_AUTO_TEST_CASE(waste_test)
954954
}
955955
}
956956

957+
958+
BOOST_AUTO_TEST_CASE(bump_fee_test)
959+
{
960+
const CAmount fee{100};
961+
const CAmount min_viable_change{200};
962+
const CAmount change_cost{125};
963+
const CAmount change_fee{35};
964+
const CAmount fee_diff{40};
965+
const CAmount target{2 * COIN};
966+
967+
{
968+
SelectionResult selection{target, SelectionAlgorithm::MANUAL};
969+
add_coin(1 * COIN, 1, selection, /*fee=*/fee, /*long_term_fee=*/fee + fee_diff);
970+
add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
971+
const std::vector<std::shared_ptr<COutput>> inputs = selection.GetShuffledInputVector();
972+
973+
for (size_t i = 0; i < inputs.size(); ++i) {
974+
inputs[i]->ApplyBumpFee(20*(i+1));
975+
}
976+
977+
selection.ComputeAndSetWaste(min_viable_change, change_cost, change_fee);
978+
CAmount expected_waste = fee_diff * -2 + change_cost + /*bump_fees=*/60;
979+
BOOST_CHECK_EQUAL(expected_waste, selection.GetWaste());
980+
}
981+
982+
{
983+
// Test with changeless transaction
984+
//
985+
// Bump fees and excess both contribute fully to the waste score,
986+
// therefore, a bump fee group discount will not change the waste
987+
// score as long as we do not create change in both instances.
988+
CAmount changeless_target = 3 * COIN - 2 * fee - 100;
989+
SelectionResult selection{changeless_target, SelectionAlgorithm::MANUAL};
990+
add_coin(1 * COIN, 1, selection, /*fee=*/fee, /*long_term_fee=*/fee + fee_diff);
991+
add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
992+
const std::vector<std::shared_ptr<COutput>> inputs = selection.GetShuffledInputVector();
993+
994+
for (size_t i = 0; i < inputs.size(); ++i) {
995+
inputs[i]->ApplyBumpFee(20*(i+1));
996+
}
997+
998+
selection.ComputeAndSetWaste(min_viable_change, change_cost, change_fee);
999+
CAmount expected_waste = fee_diff * -2 + /*bump_fees=*/60 + /*excess = 100 - bump_fees*/40;
1000+
BOOST_CHECK_EQUAL(expected_waste, selection.GetWaste());
1001+
}
1002+
}
1003+
9571004
BOOST_AUTO_TEST_CASE(effective_value_test)
9581005
{
9591006
const int input_bytes = 148;

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@
315315
'wallet_sendall.py --descriptors',
316316
'wallet_create_tx.py --descriptors',
317317
'wallet_inactive_hdchains.py --legacy-wallet',
318+
'wallet_spend_unconfirmed.py',
318319
'p2p_fingerprint.py',
319320
'feature_uacomment.py',
320321
'feature_init.py',

0 commit comments

Comments
 (0)