diff options
-rw-r--r-- | src/wallet/coinselection.cpp | 6 | ||||
-rw-r--r-- | src/wallet/coinselection.h | 21 | ||||
-rw-r--r-- | src/wallet/feebumper.cpp | 18 | ||||
-rw-r--r-- | src/wallet/spend.cpp | 17 | ||||
-rw-r--r-- | src/wallet/test/coinselector_tests.cpp | 47 | ||||
-rwxr-xr-x | test/functional/test_runner.py | 1 | ||||
-rwxr-xr-x | test/functional/wallet_spend_unconfirmed.py | 484 |
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() |