Skip to content

Commit

Permalink
Merge bitcoin#22686: wallet: Use GetSelectionAmount in ApproximateBes…
Browse files Browse the repository at this point in the history
…tSubset

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 bitcoin#22670

ACKs for top commit:
  instagibbs:
    utACK bitcoin@92885c4

Tree-SHA512: bd61fa61ffb60873e097737eebea3afe8a42296ba429de9038b3a4706763b34de9409de6cdbab21ff7f51f4787b503f840873182d9c4a1d6e12a54b017953547
  • Loading branch information
fanquake authored and vijaydasmp committed Aug 22, 2024
1 parent 85a767c commit 70f966e
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 6 deletions.
8 changes: 2 additions & 6 deletions src/wallet/coinselection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ static void ApproximateBestSubset(const std::vector<OutputGroup>& groups, const
//the selection random.
if (nPass == 0 ? insecure_rand.randbool() : !vfIncluded[i])
{
nTotal += groups[i].m_value;
nTotal += groups[i].GetSelectionAmount();
++nTotalInputCount;
vfIncluded[i] = true;
if (nTotal >= nTargetValue)
Expand All @@ -214,12 +214,8 @@ static void ApproximateBestSubset(const std::vector<OutputGroup>& groups, const
nBestInputCount = nTotalInputCount;
vfBest = vfIncluded;
}
nTotal -= groups[i].m_value;
nTotal -= groups[i].GetSelectionAmount();
--nTotalInputCount;
vfIncluded[i] = false;
}
}
}
}
}
}
Expand Down
57 changes: 57 additions & 0 deletions test/functional/rpc_fundrawtransaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ def run_test(self):
self.test_option_subtract_fee_from_outputs()
self.test_subtract_fee_with_presets()
self.test_include_unsafe()
self.test_22670()

def test_change_position(self):
"""Ensure setting changePosition in fundraw with an exact match is handled properly."""
Expand Down Expand Up @@ -850,6 +851,62 @@ def test_include_unsafe(self):
signedtx = wallet.signrawtransactionwithwallet(fundedtx['hex'])
wallet.sendrawtransaction(signedtx['hex'])

def test_22670(self):
# In issue #22670, it was observed that ApproximateBestSubset may
# choose enough value to cover the target amount but not enough to cover the transaction fees.
# This leads to a transaction whose actual transaction feerate is lower than expected.
# However at normal feerates, the difference between the effective value and the real value
# that this bug is not detected because the transaction fee must be at least 0.01 BTC (the minimum change value).
# Otherwise the targeted minimum change value will be enough to cover the transaction fees that were not
# being accounted for. So the minimum relay fee is set to 0.1 BTC/kvB in this test.
self.log.info("Test issue 22670 ApproximateBestSubset bug")
# Make sure the default wallet will not be loaded when restarted with a high minrelaytxfee
self.nodes[0].unloadwallet(self.default_wallet_name, False)
feerate = Decimal("0.1")
self.restart_node(0, [f"-minrelaytxfee={feerate}", "-discardfee=0"]) # Set high minrelayfee, set discardfee to 0 for easier calculation

self.nodes[0].loadwallet(self.default_wallet_name, True)
funds = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
self.nodes[0].createwallet(wallet_name="tester")
tester = self.nodes[0].get_wallet_rpc("tester")

# Because this test is specifically for ApproximateBestSubset, the target value must be greater
# than any single input available, and require more than 1 input. So we make 3 outputs
for i in range(0, 3):
funds.sendtoaddress(tester.getnewaddress(address_type="bech32"), 1)
self.nodes[0].generate(1)

# Create transactions in order to calculate fees for the target bounds that can trigger this bug
change_tx = tester.fundrawtransaction(tester.createrawtransaction([], [{funds.getnewaddress(): 1.5}]))
tx = tester.createrawtransaction([], [{funds.getnewaddress(): 2}])
no_change_tx = tester.fundrawtransaction(tx, {"subtractFeeFromOutputs": [0]})

overhead_fees = feerate * len(tx) / 2 / 1000
cost_of_change = change_tx["fee"] - no_change_tx["fee"]
fees = no_change_tx["fee"]
assert_greater_than(fees, 0.01)

def do_fund_send(target):
create_tx = tester.createrawtransaction([], [{funds.getnewaddress(): lower_bound}])
funded_tx = tester.fundrawtransaction(create_tx)
signed_tx = tester.signrawtransactionwithwallet(funded_tx["hex"])
assert signed_tx["complete"]
decoded_tx = tester.decoderawtransaction(signed_tx["hex"])
assert_equal(len(decoded_tx["vin"]), 3)
assert tester.testmempoolaccept([signed_tx["hex"]])[0]["allowed"]

# We want to choose more value than is available in 2 inputs when considering the fee,
# but not enough to need 3 inputs when not considering the fee.
# So the target value must be at least 2.00000001 - fee.
lower_bound = Decimal("2.00000001") - fees
# The target value must be at most 2 - cost_of_change - not_input_fees - min_change (these are all
# included in the target before ApproximateBestSubset).
upper_bound = Decimal("2.0") - cost_of_change - overhead_fees - Decimal("0.01")
assert_greater_than_or_equal(upper_bound, lower_bound)
do_fund_send(lower_bound)
do_fund_send(upper_bound)

self.restart_node(0)

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

0 comments on commit 70f966e

Please sign in to comment.