Skip to content

Commit 820129a

Browse files
committed
Merge bitcoin/bitcoin#22686: wallet: Use GetSelectionAmount in ApproximateBestSubset
92885c4 test: Test for ApproximateBestSubset edge case with too little fees (Andrew Chow) d926232 wallet: Assert that enough was selected to cover the fees (Andrew Chow) 2de222c wallet: Use GetSelectionAmount for target value calculations (Andrew Chow) Pull request description: The `m_value` used for the target calculation in `ApproximateBestSubset` is incorrect, it should be `GetSelectionAmount`. This causes a bug that is only apparent when the minimum relay fee is set to be very high. A test case is added for this, in addition to an assert in `CreateTransactionInternal` that would have also caught this issue if someone were able to hit the edge case. Fixes #22670 ACKs for top commit: instagibbs: utACK bitcoin/bitcoin@92885c4 Tree-SHA512: bd61fa61ffb60873e097737eebea3afe8a42296ba429de9038b3a4706763b34de9409de6cdbab21ff7f51f4787b503f840873182d9c4a1d6e12a54b017953547
2 parents 11c7d00 + 92885c4 commit 820129a

File tree

3 files changed

+63
-2
lines changed

3 files changed

+63
-2
lines changed

src/wallet/coinselection.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ static void ApproximateBestSubset(const std::vector<OutputGroup>& groups, const
195195
//the selection random.
196196
if (nPass == 0 ? insecure_rand.randbool() : !vfIncluded[i])
197197
{
198-
nTotal += groups[i].m_value;
198+
nTotal += groups[i].GetSelectionAmount();
199199
vfIncluded[i] = true;
200200
if (nTotal >= nTargetValue)
201201
{
@@ -205,7 +205,7 @@ static void ApproximateBestSubset(const std::vector<OutputGroup>& groups, const
205205
nBest = nTotal;
206206
vfBest = vfIncluded;
207207
}
208-
nTotal -= groups[i].m_value;
208+
nTotal -= groups[i].GetSelectionAmount();
209209
vfIncluded[i] = false;
210210
}
211211
}

src/wallet/spend.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,10 @@ bool CWallet::CreateTransactionInternal(
778778
fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes);
779779
}
780780

781+
// The only time that fee_needed should be less than the amount available for fees (in change_and_fee - change_amount) is when
782+
// we are subtracting the fee from the outputs. If this occurs at any other time, it is a bug.
783+
assert(coin_selection_params.m_subtract_fee_outputs || fee_needed <= change_and_fee - change_amount);
784+
781785
// Update nFeeRet in case fee_needed changed due to dropping the change output
782786
if (fee_needed <= change_and_fee - change_amount) {
783787
nFeeRet = change_and_fee - change_amount;

test/functional/rpc_fundrawtransaction.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ def run_test(self):
9999
self.test_subtract_fee_with_presets()
100100
self.test_transaction_too_large()
101101
self.test_include_unsafe()
102+
self.test_22670()
102103

103104
def test_change_position(self):
104105
"""Ensure setting changePosition in fundraw with an exact match is handled properly."""
@@ -969,6 +970,62 @@ def test_include_unsafe(self):
969970
signedtx = wallet.signrawtransactionwithwallet(fundedtx['hex'])
970971
wallet.sendrawtransaction(signedtx['hex'])
971972

973+
def test_22670(self):
974+
# In issue #22670, it was observed that ApproximateBestSubset may
975+
# choose enough value to cover the target amount but not enough to cover the transaction fees.
976+
# This leads to a transaction whose actual transaction feerate is lower than expected.
977+
# However at normal feerates, the difference between the effective value and the real value
978+
# that this bug is not detected because the transaction fee must be at least 0.01 BTC (the minimum change value).
979+
# Otherwise the targeted minimum change value will be enough to cover the transaction fees that were not
980+
# being accounted for. So the minimum relay fee is set to 0.1 BTC/kvB in this test.
981+
self.log.info("Test issue 22670 ApproximateBestSubset bug")
982+
# Make sure the default wallet will not be loaded when restarted with a high minrelaytxfee
983+
self.nodes[0].unloadwallet(self.default_wallet_name, False)
984+
feerate = Decimal("0.1")
985+
self.restart_node(0, [f"-minrelaytxfee={feerate}", "-discardfee=0"]) # Set high minrelayfee, set discardfee to 0 for easier calculation
986+
987+
self.nodes[0].loadwallet(self.default_wallet_name, True)
988+
funds = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
989+
self.nodes[0].createwallet(wallet_name="tester")
990+
tester = self.nodes[0].get_wallet_rpc("tester")
991+
992+
# Because this test is specifically for ApproximateBestSubset, the target value must be greater
993+
# than any single input available, and require more than 1 input. So we make 3 outputs
994+
for i in range(0, 3):
995+
funds.sendtoaddress(tester.getnewaddress(address_type="bech32"), 1)
996+
self.nodes[0].generate(1)
997+
998+
# Create transactions in order to calculate fees for the target bounds that can trigger this bug
999+
change_tx = tester.fundrawtransaction(tester.createrawtransaction([], [{funds.getnewaddress(): 1.5}]))
1000+
tx = tester.createrawtransaction([], [{funds.getnewaddress(): 2}])
1001+
no_change_tx = tester.fundrawtransaction(tx, {"subtractFeeFromOutputs": [0]})
1002+
1003+
overhead_fees = feerate * len(tx) / 2 / 1000
1004+
cost_of_change = change_tx["fee"] - no_change_tx["fee"]
1005+
fees = no_change_tx["fee"]
1006+
assert_greater_than(fees, 0.01)
1007+
1008+
def do_fund_send(target):
1009+
create_tx = tester.createrawtransaction([], [{funds.getnewaddress(): lower_bound}])
1010+
funded_tx = tester.fundrawtransaction(create_tx)
1011+
signed_tx = tester.signrawtransactionwithwallet(funded_tx["hex"])
1012+
assert signed_tx["complete"]
1013+
decoded_tx = tester.decoderawtransaction(signed_tx["hex"])
1014+
assert_equal(len(decoded_tx["vin"]), 3)
1015+
assert tester.testmempoolaccept([signed_tx["hex"]])[0]["allowed"]
1016+
1017+
# We want to choose more value than is available in 2 inputs when considering the fee,
1018+
# but not enough to need 3 inputs when not considering the fee.
1019+
# So the target value must be at least 2.00000001 - fee.
1020+
lower_bound = Decimal("2.00000001") - fees
1021+
# The target value must be at most 2 - cost_of_change - not_input_fees - min_change (these are all
1022+
# included in the target before ApproximateBestSubset).
1023+
upper_bound = Decimal("2.0") - cost_of_change - overhead_fees - Decimal("0.01")
1024+
assert_greater_than_or_equal(upper_bound, lower_bound)
1025+
do_fund_send(lower_bound)
1026+
do_fund_send(upper_bound)
1027+
1028+
self.restart_node(0)
9721029

9731030
if __name__ == '__main__':
9741031
RawTransactionsTest().main()

0 commit comments

Comments
 (0)