aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/wallet/coinselection.cpp6
-rw-r--r--src/wallet/coinselection.h21
-rw-r--r--src/wallet/feebumper.cpp18
-rw-r--r--src/wallet/spend.cpp17
-rw-r--r--src/wallet/test/coinselector_tests.cpp47
-rwxr-xr-xtest/functional/test_runner.py1
-rwxr-xr-xtest/functional/wallet_spend_unconfirmed.py484
7 files changed, 588 insertions, 6 deletions
diff --git a/src/wallet/coinselection.cpp b/src/wallet/coinselection.cpp
index 63a5866a8e..804982695e 100644
--- a/src/wallet/coinselection.cpp
+++ b/src/wallet/coinselection.cpp
@@ -514,6 +514,12 @@ CAmount SelectionResult::GetSelectedEffectiveValue() const
return std::accumulate(m_selected_inputs.cbegin(), m_selected_inputs.cend(), CAmount{0}, [](CAmount sum, const auto& coin) { return sum + coin->GetEffectiveValue(); });
}
+
+CAmount SelectionResult::GetTotalBumpFees() const
+{
+ return std::accumulate(m_selected_inputs.cbegin(), m_selected_inputs.cend(), CAmount{0}, [](CAmount sum, const auto& coin) { return sum + coin->ancestor_bump_fees; });
+}
+
void SelectionResult::Clear()
{
m_selected_inputs.clear();
diff --git a/src/wallet/coinselection.h b/src/wallet/coinselection.h
index fd75ad67a0..dd1a7bd66a 100644
--- a/src/wallet/coinselection.h
+++ b/src/wallet/coinselection.h
@@ -17,6 +17,7 @@
#include <optional>
+
namespace wallet {
//! lower bound for randomly-chosen target change amount
static constexpr CAmount CHANGE_LOWER{50000};
@@ -26,10 +27,10 @@ static constexpr CAmount CHANGE_UPPER{1000000};
/** A UTXO under consideration for use in funding a new transaction. */
struct COutput {
private:
- /** The output's value minus fees required to spend it.*/
+ /** The output's value minus fees required to spend it and bump its unconfirmed ancestors to the target feerate. */
std::optional<CAmount> effective_value;
- /** The fee required to spend this output at the transaction's target feerate. */
+ /** The fee required to spend this output at the transaction's target feerate and to bump its unconfirmed ancestors to the target feerate. */
std::optional<CAmount> fee;
public:
@@ -71,6 +72,9 @@ public:
/** The fee required to spend this output at the consolidation feerate. */
CAmount long_term_fee{0};
+ /** The fee necessary to bump this UTXO's ancestor transactions to the target feerate */
+ CAmount ancestor_bump_fees{0};
+
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)
: outpoint{outpoint},
txout{txout},
@@ -83,6 +87,7 @@ public:
from_me{from_me}
{
if (feerate) {
+ // base fee without considering potential unconfirmed ancestors
fee = input_bytes < 0 ? 0 : feerate.value().GetFee(input_bytes);
effective_value = txout.nValue - fee.value();
}
@@ -104,6 +109,16 @@ public:
return outpoint < rhs.outpoint;
}
+ void ApplyBumpFee(CAmount bump_fee)
+ {
+ assert(bump_fee >= 0);
+ ancestor_bump_fees = bump_fee;
+ assert(fee);
+ *fee += bump_fee;
+ // Note: assert(effective_value - bump_fee == nValue - fee.value());
+ effective_value = txout.nValue - fee.value();
+ }
+
CAmount GetFee() const
{
assert(fee.has_value());
@@ -355,6 +370,8 @@ public:
[[nodiscard]] CAmount GetSelectedEffectiveValue() const;
+ [[nodiscard]] CAmount GetTotalBumpFees() const;
+
void Clear();
void AddInput(const OutputGroup& group);
diff --git a/src/wallet/feebumper.cpp b/src/wallet/feebumper.cpp
index 3720d144eb..e72b412c09 100644
--- a/src/wallet/feebumper.cpp
+++ b/src/wallet/feebumper.cpp
@@ -63,7 +63,7 @@ static feebumper::Result PreconditionChecks(const CWallet& wallet, const CWallet
}
//! Check if the user provided a valid feeRate
-static feebumper::Result CheckFeeRate(const CWallet& wallet, const CFeeRate& newFeerate, const int64_t maxTxSize, CAmount old_fee, std::vector<bilingual_str>& errors)
+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)
{
// check that fee rate is higher than mempool's minimum fee
// (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
return feebumper::Result::WALLET_ERROR;
}
- CAmount new_total_fee = newFeerate.GetFee(maxTxSize);
+ std::vector<COutPoint> reused_inputs;
+ reused_inputs.reserve(mtx.vin.size());
+ for (const CTxIn& txin : mtx.vin) {
+ reused_inputs.push_back(txin.prevout);
+ }
+
+ std::map<COutPoint, CAmount> bump_fees = wallet.chain().CalculateIndividualBumpFees(reused_inputs, newFeerate);
+ CAmount total_bump_fees = 0;
+ for (auto& [_, bump_fee] : bump_fees) {
+ total_bump_fees += bump_fee;
+ }
+
+ CAmount new_total_fee = newFeerate.GetFee(maxTxSize) + total_bump_fees;
CFeeRate incrementalRelayFee = std::max(wallet.chain().relayIncrementalFee(), CFeeRate(WALLET_INCREMENTAL_RELAY_FEE));
@@ -283,7 +295,7 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo
}
temp_mtx.vout = txouts;
const int64_t maxTxSize{CalculateMaximumSignedTxSize(CTransaction(temp_mtx), &wallet, &new_coin_control).vsize};
- Result res = CheckFeeRate(wallet, *new_coin_control.m_feerate, maxTxSize, old_fee, errors);
+ Result res = CheckFeeRate(wallet, temp_mtx, *new_coin_control.m_feerate, maxTxSize, old_fee, errors);
if (res != Result::OK) {
return res;
}
diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp
index c0ee00e097..dc2b669140 100644
--- a/src/wallet/spend.cpp
+++ b/src/wallet/spend.cpp
@@ -162,6 +162,7 @@ util::Result<PreSelectedInputs> FetchSelectedInputs(const CWallet& wallet, const
{
PreSelectedInputs result;
const bool can_grind_r = wallet.CanGrindR();
+ std::map<COutPoint, CAmount> map_of_bump_fees = wallet.chain().CalculateIndividualBumpFees(coin_control.ListSelected(), coin_selection_params.m_effective_feerate);
for (const COutPoint& outpoint : coin_control.ListSelected()) {
int input_bytes = -1;
CTxOut txout;
@@ -197,6 +198,7 @@ util::Result<PreSelectedInputs> FetchSelectedInputs(const CWallet& wallet, const
/* 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. */
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);
+ output.ApplyBumpFee(map_of_bump_fees.at(output.outpoint));
result.Insert(output, coin_selection_params.m_subtract_fee_outputs);
}
return result;
@@ -217,6 +219,7 @@ CoinsResult AvailableCoins(const CWallet& wallet,
const int max_depth = {coinControl ? coinControl->m_max_depth : DEFAULT_MAX_DEPTH};
const bool only_safe = {coinControl ? !coinControl->m_include_unsafe_inputs : true};
const bool can_grind_r = wallet.CanGrindR();
+ std::vector<COutPoint> outpoints;
std::set<uint256> trusted_parents;
for (const auto& entry : wallet.mapWallet)
@@ -334,6 +337,8 @@ CoinsResult AvailableCoins(const CWallet& wallet,
result.Add(GetOutputType(type, is_from_p2sh),
COutput(outpoint, output, nDepth, input_bytes, spendable, solvable, safeTx, wtx.GetTxTime(), tx_from_me, feerate));
+ outpoints.push_back(outpoint);
+
// Checks the sum amount of all UTXO's.
if (params.min_sum_amount != MAX_MONEY) {
if (result.GetTotalAmount() >= params.min_sum_amount) {
@@ -348,6 +353,16 @@ CoinsResult AvailableCoins(const CWallet& wallet,
}
}
+ if (feerate.has_value()) {
+ std::map<COutPoint, CAmount> map_of_bump_fees = wallet.chain().CalculateIndividualBumpFees(outpoints, feerate.value());
+
+ for (auto& [_, outputs] : result.coins) {
+ for (auto& output : outputs) {
+ output.ApplyBumpFee(map_of_bump_fees.at(output.outpoint));
+ }
+ }
+ }
+
return result;
}
@@ -1021,7 +1036,7 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
if (nBytes == -1) {
return util::Error{_("Missing solving data for estimating transaction size")};
}
- CAmount fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes);
+ CAmount fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes) + result.GetTotalBumpFees();
const CAmount output_value = CalculateOutputValue(txNew);
Assume(recipients_sum + change_amount == output_value);
CAmount current_fee = result.GetSelectedValue() - output_value;
diff --git a/src/wallet/test/coinselector_tests.cpp b/src/wallet/test/coinselector_tests.cpp
index 3964e9adde..0f344b7c15 100644
--- a/src/wallet/test/coinselector_tests.cpp
+++ b/src/wallet/test/coinselector_tests.cpp
@@ -954,6 +954,53 @@ BOOST_AUTO_TEST_CASE(waste_test)
}
}
+
+BOOST_AUTO_TEST_CASE(bump_fee_test)
+{
+ const CAmount fee{100};
+ const CAmount min_viable_change{200};
+ const CAmount change_cost{125};
+ const CAmount change_fee{35};
+ const CAmount fee_diff{40};
+ const CAmount target{2 * COIN};
+
+ {
+ SelectionResult selection{target, SelectionAlgorithm::MANUAL};
+ add_coin(1 * COIN, 1, selection, /*fee=*/fee, /*long_term_fee=*/fee + fee_diff);
+ add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
+ const std::vector<std::shared_ptr<COutput>> inputs = selection.GetShuffledInputVector();
+
+ for (size_t i = 0; i < inputs.size(); ++i) {
+ inputs[i]->ApplyBumpFee(20*(i+1));
+ }
+
+ selection.ComputeAndSetWaste(min_viable_change, change_cost, change_fee);
+ CAmount expected_waste = fee_diff * -2 + change_cost + /*bump_fees=*/60;
+ BOOST_CHECK_EQUAL(expected_waste, selection.GetWaste());
+ }
+
+ {
+ // Test with changeless transaction
+ //
+ // Bump fees and excess both contribute fully to the waste score,
+ // therefore, a bump fee group discount will not change the waste
+ // score as long as we do not create change in both instances.
+ CAmount changeless_target = 3 * COIN - 2 * fee - 100;
+ SelectionResult selection{changeless_target, SelectionAlgorithm::MANUAL};
+ add_coin(1 * COIN, 1, selection, /*fee=*/fee, /*long_term_fee=*/fee + fee_diff);
+ add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
+ const std::vector<std::shared_ptr<COutput>> inputs = selection.GetShuffledInputVector();
+
+ for (size_t i = 0; i < inputs.size(); ++i) {
+ inputs[i]->ApplyBumpFee(20*(i+1));
+ }
+
+ selection.ComputeAndSetWaste(min_viable_change, change_cost, change_fee);
+ CAmount expected_waste = fee_diff * -2 + /*bump_fees=*/60 + /*excess = 100 - bump_fees*/40;
+ BOOST_CHECK_EQUAL(expected_waste, selection.GetWaste());
+ }
+}
+
BOOST_AUTO_TEST_CASE(effective_value_test)
{
const int input_bytes = 148;
diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py
index db04bb8bdb..ed0d91c290 100755
--- a/test/functional/test_runner.py
+++ b/test/functional/test_runner.py
@@ -315,6 +315,7 @@ BASE_SCRIPTS = [
'wallet_sendall.py --descriptors',
'wallet_create_tx.py --descriptors',
'wallet_inactive_hdchains.py --legacy-wallet',
+ 'wallet_spend_unconfirmed.py',
'p2p_fingerprint.py',
'feature_uacomment.py',
'feature_init.py',
diff --git a/test/functional/wallet_spend_unconfirmed.py b/test/functional/wallet_spend_unconfirmed.py
new file mode 100755
index 0000000000..af99ed7f1c
--- /dev/null
+++ b/test/functional/wallet_spend_unconfirmed.py
@@ -0,0 +1,484 @@
+#!/usr/bin/env python3
+# Copyright (c) 2022 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+from decimal import Decimal, getcontext
+
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.util import (
+ assert_greater_than_or_equal,
+ assert_equal,
+ find_vout_for_address,
+)
+
+class UnconfirmedInputTest(BitcoinTestFramework):
+ def add_options(self, parser):
+ self.add_wallet_options(parser)
+
+ def set_test_params(self):
+ getcontext().prec=9
+ self.setup_clean_chain = True
+ self.num_nodes = 1
+
+ def setup_and_fund_wallet(self, walletname):
+ self.nodes[0].createwallet(walletname)
+ wallet = self.nodes[0].get_wallet_rpc(walletname)
+
+ self.def_wallet.sendtoaddress(address=wallet.getnewaddress(), amount=2)
+ self.generate(self.nodes[0], 1) # confirm funding tx
+ return wallet
+
+ def skip_test_if_missing_module(self):
+ self.skip_if_no_wallet()
+
+ def calc_fee_rate(self, tx):
+ fee = Decimal(-1e8) * tx["fee"]
+ vsize = tx["decoded"]["vsize"]
+ return fee / vsize
+
+ def calc_set_fee_rate(self, txs):
+ fees = Decimal(-1e8) * sum([tx["fee"] for tx in txs]) # fee is negative!
+ vsizes = sum([tx["decoded"]["vsize"] for tx in txs])
+ return fees / vsizes
+
+ def assert_spends_only_parents(self, tx, parent_txids):
+ parent_checklist = parent_txids.copy()
+ number_inputs = len(tx["decoded"]["vin"])
+ assert_equal(number_inputs, len(parent_txids))
+ for i in range(number_inputs):
+ txid_of_input = tx["decoded"]["vin"][i]["txid"]
+ assert txid_of_input in parent_checklist
+ parent_checklist.remove(txid_of_input)
+
+ def assert_undershoots_target(self, tx):
+ resulting_fee_rate = self.calc_fee_rate(tx)
+ assert_greater_than_or_equal(self.target_fee_rate, resulting_fee_rate)
+
+ def assert_beats_target(self, tx):
+ resulting_fee_rate = self.calc_fee_rate(tx)
+ assert_greater_than_or_equal(resulting_fee_rate, self.target_fee_rate)
+
+ # Meta-Test: try feerate testing function on confirmed UTXO
+ def test_target_feerate_confirmed(self):
+ self.log.info("Start test feerate with confirmed input")
+ wallet = self.setup_and_fund_wallet("confirmed_wallet")
+
+ ancestor_aware_txid = wallet.sendtoaddress(address=self.def_wallet.getnewaddress(), amount=0.5, fee_rate=self.target_fee_rate)
+ ancestor_aware_tx = wallet.gettransaction(txid=ancestor_aware_txid, verbose=True)
+ self.assert_beats_target(ancestor_aware_tx)
+
+ wallet.unloadwallet()
+
+ # Spend unconfirmed UTXO from high-feerate parent
+ def test_target_feerate_unconfirmed_high(self):
+ self.log.info("Start test feerate with high feerate unconfirmed input")
+ wallet = self.setup_and_fund_wallet("unconfirmed_high_wallet")
+
+ # Send unconfirmed transaction with high feerate to testing wallet
+ parent_txid = wallet.sendtoaddress(address=wallet.getnewaddress(), amount=1, fee_rate=3*self.target_fee_rate)
+ parent_tx = wallet.gettransaction(txid=parent_txid, verbose=True)
+ self.assert_beats_target(parent_tx)
+
+ ancestor_aware_txid = wallet.sendtoaddress(address=self.def_wallet.getnewaddress(), amount=0.5, fee_rate=self.target_fee_rate)
+ ancestor_aware_tx = wallet.gettransaction(txid=ancestor_aware_txid, verbose=True)
+
+ self.assert_spends_only_parents(ancestor_aware_tx, [parent_txid])
+
+ self.assert_beats_target(ancestor_aware_tx)
+
+ wallet.unloadwallet()
+
+ # Spend unconfirmed UTXO from low-feerate parent. Expect that parent gets
+ # bumped to target feerate.
+ def test_target_feerate_unconfirmed_low(self):
+ self.log.info("Start test feerate with low feerate unconfirmed input")
+ wallet = self.setup_and_fund_wallet("unconfirmed_low_wallet")
+
+ parent_txid = wallet.sendtoaddress(address=wallet.getnewaddress(), amount=1, fee_rate=1)
+ parent_tx = wallet.gettransaction(txid=parent_txid, verbose=True)
+
+ self.assert_undershoots_target(parent_tx)
+
+ ancestor_aware_txid = wallet.sendtoaddress(address=self.def_wallet.getnewaddress(), amount=0.5, fee_rate=self.target_fee_rate)
+ ancestor_aware_tx = wallet.gettransaction(txid=ancestor_aware_txid, verbose=True)
+
+ self.assert_spends_only_parents(ancestor_aware_tx, [parent_txid])
+
+ self.assert_beats_target(ancestor_aware_tx)
+ resulting_ancestry_fee_rate = self.calc_set_fee_rate([parent_tx, ancestor_aware_tx])
+ assert_greater_than_or_equal(resulting_ancestry_fee_rate, self.target_fee_rate)
+ assert_greater_than_or_equal(self.target_fee_rate*1.01, resulting_ancestry_fee_rate)
+
+ wallet.unloadwallet()
+
+ # Spend UTXO with unconfirmed low feerate parent and grandparent
+ # txs. Expect that both ancestors get bumped to target feerate.
+ def test_chain_of_unconfirmed_low(self):
+ self.log.info("Start test with parent and grandparent tx")
+ wallet = self.setup_and_fund_wallet("unconfirmed_low_chain_wallet")
+
+ grandparent_txid = wallet.sendtoaddress(address=wallet.getnewaddress(), amount=1.8, fee_rate=1)
+ gp_tx = wallet.gettransaction(txid=grandparent_txid, verbose=True)
+
+ self.assert_undershoots_target(gp_tx)
+
+ parent_txid = wallet.sendtoaddress(address=wallet.getnewaddress(), amount=1.5, fee_rate=2)
+ p_tx = wallet.gettransaction(txid=parent_txid, verbose=True)
+
+ self.assert_undershoots_target(p_tx)
+
+ ancestor_aware_txid = wallet.sendtoaddress(address=self.def_wallet.getnewaddress(), amount=1.3, fee_rate=self.target_fee_rate)
+ ancestor_aware_tx = wallet.gettransaction(txid=ancestor_aware_txid, verbose=True)
+ self.assert_spends_only_parents(ancestor_aware_tx, [parent_txid])
+
+ self.assert_beats_target(ancestor_aware_tx)
+ resulting_ancestry_fee_rate = self.calc_set_fee_rate([gp_tx, p_tx, ancestor_aware_tx])
+ assert_greater_than_or_equal(resulting_ancestry_fee_rate, self.target_fee_rate)
+ assert_greater_than_or_equal(self.target_fee_rate*1.01, resulting_ancestry_fee_rate)
+
+ wallet.unloadwallet()
+
+ # Spend unconfirmed UTXOs from two low feerate parent txs.
+ def test_two_low_feerate_unconfirmed_parents(self):
+ self.log.info("Start test with two unconfirmed parent txs")
+ wallet = self.setup_and_fund_wallet("two_parents_wallet")
+
+ # Add second UTXO to tested wallet
+ self.def_wallet.sendtoaddress(address=wallet.getnewaddress(), amount=2)
+ self.generate(self.nodes[0], 1) # confirm funding tx
+
+ parent_one_txid = wallet.sendtoaddress(address=wallet.getnewaddress(), amount=1.5, fee_rate=2)
+ p_one_tx = wallet.gettransaction(txid=parent_one_txid, verbose=True)
+ self.assert_undershoots_target(p_one_tx)
+
+ parent_two_txid = wallet.sendtoaddress(address=wallet.getnewaddress(), amount=1.5, fee_rate=1)
+ p_two_tx = wallet.gettransaction(txid=parent_two_txid, verbose=True)
+ self.assert_undershoots_target(p_two_tx)
+
+ ancestor_aware_txid = wallet.sendtoaddress(address=self.def_wallet.getnewaddress(), amount=2.8, fee_rate=self.target_fee_rate)
+ ancestor_aware_tx = wallet.gettransaction(txid=ancestor_aware_txid, verbose=True)
+ self.assert_spends_only_parents(ancestor_aware_tx, [parent_one_txid, parent_two_txid])
+
+ self.assert_beats_target(ancestor_aware_tx)
+ resulting_ancestry_fee_rate = self.calc_set_fee_rate([p_one_tx, p_two_tx, ancestor_aware_tx])
+ assert_greater_than_or_equal(resulting_ancestry_fee_rate, self.target_fee_rate)
+ assert_greater_than_or_equal(self.target_fee_rate*1.01, resulting_ancestry_fee_rate)
+
+ wallet.unloadwallet()
+
+ # Spend two unconfirmed inputs, one each from low and high feerate parents
+ def test_mixed_feerate_unconfirmed_parents(self):
+ self.log.info("Start test with two unconfirmed parent txs one of which has a higher feerate")
+ wallet = self.setup_and_fund_wallet("two_mixed_parents_wallet")
+
+ # Add second UTXO to tested wallet
+ self.def_wallet.sendtoaddress(address=wallet.getnewaddress(), amount=2)
+ self.generate(self.nodes[0], 1) # confirm funding tx
+
+ high_parent_txid = wallet.sendtoaddress(address=wallet.getnewaddress(), amount=1.5, fee_rate=self.target_fee_rate*2)
+ p_high_tx = wallet.gettransaction(txid=high_parent_txid, verbose=True)
+ # This time the parent is greater than the child
+ self.assert_beats_target(p_high_tx)
+
+ parent_low_txid = wallet.sendtoaddress(address=wallet.getnewaddress(), amount=1.5, fee_rate=1)
+ p_low_tx = wallet.gettransaction(txid=parent_low_txid, verbose=True)
+ # Other parent needs bump
+ self.assert_undershoots_target(p_low_tx)
+
+ ancestor_aware_txid = wallet.sendtoaddress(address=self.def_wallet.getnewaddress(), amount=2.8, fee_rate=self.target_fee_rate)
+ ancestor_aware_tx = wallet.gettransaction(txid=ancestor_aware_txid, verbose=True)
+ self.assert_spends_only_parents(ancestor_aware_tx, [parent_low_txid, high_parent_txid])
+
+ self.assert_beats_target(ancestor_aware_tx)
+ resulting_ancestry_fee_rate = self.calc_set_fee_rate([p_high_tx, p_low_tx, ancestor_aware_tx])
+ assert_greater_than_or_equal(resulting_ancestry_fee_rate, self.target_fee_rate)
+
+ resulting_bumped_ancestry_fee_rate = self.calc_set_fee_rate([p_low_tx, ancestor_aware_tx])
+ assert_greater_than_or_equal(resulting_bumped_ancestry_fee_rate, self.target_fee_rate)
+ assert_greater_than_or_equal(self.target_fee_rate*1.01, resulting_bumped_ancestry_fee_rate)
+
+ wallet.unloadwallet()
+
+ # Spend from chain with high feerate grandparent and low feerate parent
+ def test_chain_of_high_low(self):
+ self.log.info("Start test with low parent and high grandparent tx")
+ wallet = self.setup_and_fund_wallet("high_low_chain_wallet")
+
+ grandparent_txid = wallet.sendtoaddress(address=wallet.getnewaddress(), amount=1.8, fee_rate=self.target_fee_rate * 10)
+ gp_tx = wallet.gettransaction(txid=grandparent_txid, verbose=True)
+ # grandparent has higher feerate
+ self.assert_beats_target(gp_tx)
+
+ parent_txid = wallet.sendtoaddress(address=wallet.getnewaddress(), amount=1.5, fee_rate=1)
+ # parent is low feerate
+ p_tx = wallet.gettransaction(txid=parent_txid, verbose=True)
+ self.assert_undershoots_target(p_tx)
+
+ ancestor_aware_txid = wallet.sendtoaddress(address=self.def_wallet.getnewaddress(), amount=1.3, fee_rate=self.target_fee_rate)
+ ancestor_aware_tx = wallet.gettransaction(txid=ancestor_aware_txid, verbose=True)
+ self.assert_spends_only_parents(ancestor_aware_tx, [parent_txid])
+
+ self.assert_beats_target(ancestor_aware_tx)
+ resulting_ancestry_fee_rate = self.calc_set_fee_rate([p_tx, ancestor_aware_tx])
+ assert_greater_than_or_equal(resulting_ancestry_fee_rate, self.target_fee_rate)
+ assert_greater_than_or_equal(self.target_fee_rate*1.01, resulting_ancestry_fee_rate)
+ resulting_ancestry_fee_rate_with_high_feerate_gp = self.calc_set_fee_rate([gp_tx, p_tx, ancestor_aware_tx])
+ # Check that we bumped the parent without relying on the grandparent
+ assert_greater_than_or_equal(resulting_ancestry_fee_rate_with_high_feerate_gp, self.target_fee_rate*1.1)
+
+ wallet.unloadwallet()
+
+ # Spend UTXO from chain of unconfirmed transactions with low feerate
+ # grandparent and even lower feerate parent
+ def test_chain_of_high_low_below_target_feerate(self):
+ self.log.info("Start test with low parent and higher low grandparent tx")
+ wallet = self.setup_and_fund_wallet("low_and_lower_chain_wallet")
+
+ grandparent_txid = wallet.sendtoaddress(address=wallet.getnewaddress(), amount=1.8, fee_rate=5)
+ gp_tx = wallet.gettransaction(txid=grandparent_txid, verbose=True)
+
+ # grandparent has higher feerate, but below target
+ self.assert_undershoots_target(gp_tx)
+
+ parent_txid = wallet.sendtoaddress(address=wallet.getnewaddress(), amount=1.5, fee_rate=1)
+ p_tx = wallet.gettransaction(txid=parent_txid, verbose=True)
+ # parent even lower
+ self.assert_undershoots_target(p_tx)
+
+ ancestor_aware_txid = wallet.sendtoaddress(address=self.def_wallet.getnewaddress(), amount=1.3, fee_rate=self.target_fee_rate)
+ ancestor_aware_tx = wallet.gettransaction(txid=ancestor_aware_txid, verbose=True)
+ self.assert_spends_only_parents(ancestor_aware_tx, [parent_txid])
+
+ self.assert_beats_target(ancestor_aware_tx)
+ resulting_ancestry_fee_rate = self.calc_set_fee_rate([gp_tx, p_tx, ancestor_aware_tx])
+ assert_greater_than_or_equal(resulting_ancestry_fee_rate, self.target_fee_rate)
+ assert_greater_than_or_equal(self.target_fee_rate*1.01, resulting_ancestry_fee_rate)
+
+ wallet.unloadwallet()
+
+ # Test fee calculation when bumping while using subtract fee from output (SFFO)
+ def test_target_feerate_unconfirmed_low_sffo(self):
+ self.log.info("Start test feerate with low feerate unconfirmed input, while subtracting from output")
+ wallet = self.setup_and_fund_wallet("unconfirmed_low_wallet_sffo")
+
+ parent_txid = wallet.sendtoaddress(address=wallet.getnewaddress(), amount=1, fee_rate=1)
+ parent_tx = wallet.gettransaction(txid=parent_txid, verbose=True)
+
+ self.assert_undershoots_target(parent_tx)
+
+ ancestor_aware_txid = wallet.sendtoaddress(address=self.def_wallet.getnewaddress(), amount=0.5, fee_rate=self.target_fee_rate, subtractfeefromamount=True)
+ ancestor_aware_tx = wallet.gettransaction(txid=ancestor_aware_txid, verbose=True)
+
+ self.assert_spends_only_parents(ancestor_aware_tx, [parent_txid])
+
+ self.assert_beats_target(ancestor_aware_tx)
+ resulting_ancestry_fee_rate = self.calc_set_fee_rate([parent_tx, ancestor_aware_tx])
+ assert_greater_than_or_equal(resulting_ancestry_fee_rate, self.target_fee_rate)
+ assert_greater_than_or_equal(self.target_fee_rate*1.01, resulting_ancestry_fee_rate)
+
+ wallet.unloadwallet()
+
+ # Test that parents of preset unconfirmed inputs get cpfp'ed
+ def test_preset_input_cpfp(self):
+ self.log.info("Start test with preset input from low feerate unconfirmed transaction")
+ wallet = self.setup_and_fund_wallet("preset_input")
+
+ parent_txid = wallet.sendtoaddress(address=wallet.getnewaddress(), amount=1, fee_rate=1)
+ parent_tx = wallet.gettransaction(txid=parent_txid, verbose=True)
+
+ self.assert_undershoots_target(parent_tx)
+
+ number_outputs = len(parent_tx["decoded"]["vout"])
+ assert_equal(number_outputs, 2)
+
+ # we don't care which of the two outputs we spent, they're both ours
+ ancestor_aware_txid = wallet.send(outputs=[{self.def_wallet.getnewaddress(): 0.5}], fee_rate=self.target_fee_rate, options={"add_inputs": True, "inputs": [{"txid": parent_txid, "vout": 0}]})["txid"]
+ ancestor_aware_tx = wallet.gettransaction(txid=ancestor_aware_txid, verbose=True)
+
+ self.assert_spends_only_parents(ancestor_aware_tx, [parent_txid])
+
+ self.assert_beats_target(ancestor_aware_tx)
+ resulting_ancestry_fee_rate = self.calc_set_fee_rate([parent_tx, ancestor_aware_tx])
+ assert_greater_than_or_equal(resulting_ancestry_fee_rate, self.target_fee_rate)
+ assert_greater_than_or_equal(self.target_fee_rate*1.01, resulting_ancestry_fee_rate)
+
+ wallet.unloadwallet()
+
+ # Test that RBFing a transaction with unconfirmed input gets the right feerate
+ def test_rbf_bumping(self):
+ self.log.info("Start test to rbf a transaction unconfirmed input to bump it")
+ wallet = self.setup_and_fund_wallet("bump")
+
+ parent_txid = wallet.sendtoaddress(address=wallet.getnewaddress(), amount=1, fee_rate=1)
+ parent_tx = wallet.gettransaction(txid=parent_txid, verbose=True)
+
+ self.assert_undershoots_target(parent_tx)
+
+ to_be_rbfed_ancestor_aware_txid = wallet.sendtoaddress(address=self.def_wallet.getnewaddress(), amount=0.5, fee_rate=self.target_fee_rate)
+ ancestor_aware_tx = wallet.gettransaction(txid=to_be_rbfed_ancestor_aware_txid, verbose=True)
+
+ self.assert_spends_only_parents(ancestor_aware_tx, [parent_txid])
+
+ self.assert_beats_target(ancestor_aware_tx)
+ resulting_ancestry_fee_rate = self.calc_set_fee_rate([parent_tx, ancestor_aware_tx])
+ assert_greater_than_or_equal(resulting_ancestry_fee_rate, self.target_fee_rate)
+ assert_greater_than_or_equal(self.target_fee_rate*1.01, resulting_ancestry_fee_rate)
+
+ bumped_ancestor_aware_txid = wallet.bumpfee(txid=to_be_rbfed_ancestor_aware_txid, options={"fee_rate": self.target_fee_rate * 2} )["txid"]
+ bumped_ancestor_aware_tx = wallet.gettransaction(txid=bumped_ancestor_aware_txid, verbose=True)
+ self.assert_spends_only_parents(ancestor_aware_tx, [parent_txid])
+
+ resulting_bumped_fee_rate = self.calc_fee_rate(bumped_ancestor_aware_tx)
+ assert_greater_than_or_equal(resulting_bumped_fee_rate, 2*self.target_fee_rate)
+ resulting_bumped_ancestry_fee_rate = self.calc_set_fee_rate([parent_tx, bumped_ancestor_aware_tx])
+ assert_greater_than_or_equal(resulting_bumped_ancestry_fee_rate, 2*self.target_fee_rate)
+ assert_greater_than_or_equal(2*self.target_fee_rate*1.01, resulting_bumped_ancestry_fee_rate)
+
+ wallet.unloadwallet()
+
+ # Test that new transaction ignores sibling transaction with low feerate
+ def test_sibling_tx_gets_ignored(self):
+ self.log.info("Start test where a low-fee sibling tx gets created and check that bumping ignores it")
+ wallet = self.setup_and_fund_wallet("ignore-sibling")
+
+ parent_txid = wallet.sendtoaddress(address=wallet.getnewaddress(), amount=1, fee_rate=2)
+ parent_tx = wallet.gettransaction(txid=parent_txid, verbose=True)
+
+ self.assert_undershoots_target(parent_tx)
+
+ # create sibling tx
+ sibling_txid = wallet.sendtoaddress(address=self.def_wallet.getnewaddress(), amount=0.9, fee_rate=1)
+ sibling_tx = wallet.gettransaction(txid=sibling_txid, verbose=True)
+ self.assert_undershoots_target(sibling_tx)
+
+ # spend both outputs from parent transaction
+ ancestor_aware_txid = wallet.sendtoaddress(address=self.def_wallet.getnewaddress(), amount=0.5, fee_rate=self.target_fee_rate)
+ ancestor_aware_tx = wallet.gettransaction(txid=ancestor_aware_txid, verbose=True)
+
+ self.assert_spends_only_parents(ancestor_aware_tx, [parent_txid])
+
+ self.assert_beats_target(ancestor_aware_tx)
+ resulting_ancestry_fee_rate = self.calc_set_fee_rate([parent_tx, ancestor_aware_tx])
+ assert_greater_than_or_equal(resulting_ancestry_fee_rate, self.target_fee_rate)
+ assert_greater_than_or_equal(self.target_fee_rate*1.01, resulting_ancestry_fee_rate)
+
+ wallet.unloadwallet()
+
+ # Test that new transaction only pays for itself when high feerate sibling pays for parent
+ def test_sibling_tx_bumps_parent(self):
+ self.log.info("Start test where a high-fee sibling tx bumps the parent")
+ wallet = self.setup_and_fund_wallet("generous-sibling")
+
+ parent_txid = wallet.sendtoaddress(address=wallet.getnewaddress(), amount=1, fee_rate=1)
+ parent_tx = wallet.gettransaction(txid=parent_txid, verbose=True)
+ self.assert_undershoots_target(parent_tx)
+
+ # create sibling tx
+ sibling_txid = wallet.sendtoaddress(address=self.def_wallet.getnewaddress(), amount=0.9, fee_rate=3*self.target_fee_rate)
+ sibling_tx = wallet.gettransaction(txid=sibling_txid, verbose=True)
+ self.assert_beats_target(sibling_tx)
+
+ # spend both outputs from parent transaction
+ ancestor_aware_txid = wallet.sendtoaddress(address=self.def_wallet.getnewaddress(), amount=0.5, fee_rate=self.target_fee_rate)
+ ancestor_aware_tx = wallet.gettransaction(txid=ancestor_aware_txid, verbose=True)
+
+ self.assert_spends_only_parents(ancestor_aware_tx, [parent_txid])
+
+ self.assert_beats_target(ancestor_aware_tx)
+ # Child is only paying for itself…
+ resulting_fee_rate = self.calc_fee_rate(ancestor_aware_tx)
+ assert_greater_than_or_equal(1.05 * self.target_fee_rate, resulting_fee_rate)
+ # …because sibling bumped to parent to ~50 s/vB, while our target is 30 s/vB
+ resulting_ancestry_fee_rate_sibling = self.calc_set_fee_rate([parent_tx, sibling_tx])
+ assert_greater_than_or_equal(resulting_ancestry_fee_rate_sibling, self.target_fee_rate)
+ # and our resulting "ancestry feerate" is therefore BELOW target feerate
+ resulting_ancestry_fee_rate = self.calc_set_fee_rate([parent_tx, ancestor_aware_tx])
+ assert_greater_than_or_equal(self.target_fee_rate, resulting_ancestry_fee_rate)
+
+ wallet.unloadwallet()
+
+ # Spend a confirmed and an unconfirmed input at the same time
+ def test_confirmed_and_unconfirmed_parent(self):
+ self.log.info("Start test with one unconfirmed and one confirmed input")
+ wallet = self.setup_and_fund_wallet("confirmed_and_unconfirmed_wallet")
+ confirmed_parent_txid = wallet.sendtoaddress(address=wallet.getnewaddress(), amount=1, fee_rate=self.target_fee_rate)
+ self.generate(self.nodes[0], 1) # Wallet has two confirmed UTXOs of ~1BTC each
+ unconfirmed_parent_txid = wallet.sendtoaddress(address=wallet.getnewaddress(), amount=0.5, fee_rate=0.5*self.target_fee_rate)
+
+ # wallet has one confirmed UTXO of 1BTC and two unconfirmed UTXOs of ~0.5BTC each
+ ancestor_aware_txid = wallet.sendtoaddress(address=self.def_wallet.getnewaddress(), amount=1.4, fee_rate=self.target_fee_rate)
+ ancestor_aware_tx = wallet.gettransaction(txid=ancestor_aware_txid, verbose=True)
+ self.assert_spends_only_parents(ancestor_aware_tx, [confirmed_parent_txid, unconfirmed_parent_txid])
+ resulting_fee_rate = self.calc_fee_rate(ancestor_aware_tx)
+ assert_greater_than_or_equal(resulting_fee_rate, self.target_fee_rate)
+
+ wallet.unloadwallet()
+
+ def test_external_input_unconfirmed_low(self):
+ self.log.info("Send funds to an external wallet then build tx that bumps parent by spending external input")
+ wallet = self.setup_and_fund_wallet("test_external_wallet")
+
+ external_address = self.def_wallet.getnewaddress()
+ address_info = self.def_wallet.getaddressinfo(external_address)
+ external_descriptor = address_info["desc"]
+ parent_txid = wallet.sendtoaddress(address=external_address, amount=1, fee_rate=1)
+ parent_tx = wallet.gettransaction(txid=parent_txid, verbose=True)
+
+ self.assert_undershoots_target(parent_tx)
+
+ spend_res = wallet.send(outputs=[{self.def_wallet.getnewaddress(): 0.5}], fee_rate=self.target_fee_rate, options={"inputs":[{"txid":parent_txid, "vout":find_vout_for_address(self.nodes[0], parent_txid, external_address)}], "solving_data":{"descriptors":[external_descriptor]}})
+ signed_psbt = self.def_wallet.walletprocesspsbt(spend_res["psbt"])
+ external_tx = self.def_wallet.finalizepsbt(signed_psbt["psbt"])
+ ancestor_aware_txid = self.def_wallet.sendrawtransaction(external_tx["hex"])
+
+ ancestor_aware_tx = self.def_wallet.gettransaction(txid=ancestor_aware_txid, verbose=True)
+
+ self.assert_spends_only_parents(ancestor_aware_tx, [parent_txid])
+
+ self.assert_beats_target(ancestor_aware_tx)
+ resulting_ancestry_fee_rate = self.calc_set_fee_rate([parent_tx, ancestor_aware_tx])
+ assert_greater_than_or_equal(resulting_ancestry_fee_rate, self.target_fee_rate)
+ assert_greater_than_or_equal(self.target_fee_rate*1.01, resulting_ancestry_fee_rate)
+
+ wallet.unloadwallet()
+
+
+ def run_test(self):
+ self.log.info("Starting UnconfirmedInputTest!")
+ self.target_fee_rate = 30
+ self.def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
+ self.generate(self.nodes[0], 110)
+
+ self.test_target_feerate_confirmed()
+
+ self.test_target_feerate_unconfirmed_high()
+
+ self.test_target_feerate_unconfirmed_low()
+
+ self.test_chain_of_unconfirmed_low()
+
+ self.test_two_low_feerate_unconfirmed_parents()
+
+ self.test_mixed_feerate_unconfirmed_parents()
+
+ self.test_chain_of_high_low()
+
+ self.test_chain_of_high_low_below_target_feerate()
+
+ self.test_target_feerate_unconfirmed_low_sffo()
+
+ self.test_preset_input_cpfp()
+
+ self.test_rbf_bumping()
+
+ self.test_sibling_tx_gets_ignored()
+
+ self.test_sibling_tx_bumps_parent()
+
+ self.test_confirmed_and_unconfirmed_parent()
+
+ self.test_external_input_unconfirmed_low()
+
+if __name__ == '__main__':
+ UnconfirmedInputTest().main()