mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-04-27 07:18:33 +02:00
Merge bitcoin/bitcoin#26560: wallet: bugfix, invalid CoinsResult cached total amount
7362f8e5e2refactor: make CoinsResult total amounts members private (furszy)3282fad599wallet: add assert to SelectionResult::Merge for safety (S3RK)c4e3b7d6a1wallet: SelectCoins, return early if wallet's UTXOs cannot cover the target (furszy)cac2725fd0test: bugfix, coinselector_test, use 'CoinsResult::Erase/Add' instead of direct member access (furszy)cf79384697test: Coin Selection, duplicated preset inputs selection (furszy)341ba7ffd8test: wallet, coverage for CoinsResult::Erase function (furszy)f930aefff9wallet: bugfix, 'CoinsResult::Erase' is erasing only one output of the set (furszy) Pull request description: This comes with #26559. Solving few bugs inside the wallet's transaction creation process and adding test coverage for them. Plus, making use of the `CoinsResult::total_amount` cached value inside the Coin Selection process to return early if we don't have enough funds to cover the target amount. ### Bugs 1) The `CoinsResult::Erase` method removes only one output from the available coins vector (there is a [loop break](c1061be14a/src/wallet/spend.cpp (L112)) that should have never been there) and not all the preset inputs. Which on master is not a problem, because since [#25685](https://github.com/bitcoin/bitcoin/pull/25685) we are no longer using the method. But, it's a bug on v24 (check [#26559](https://github.com/bitcoin/bitcoin/pull/26559)). This method it's being fixed and not removed because I'm later using it to solve another bug inside this PR. 2) As we update the total cached amount of the `CoinsResult` object inside `AvailableCoins` and we don't use such function inside the coin selection tests (we manually load up the `CoinsResult` object), there is a discrepancy between the outputs that we add/erase and the total amount cached value. ### Improvements * This makes use of the `CoinsResult` total amount field to early return with an "Insufficient funds" error inside Coin Selection if the tx target amount is greater than the sum of all the wallet available coins plus the preset inputs amounts (we don't need to perform the entire coin selection process if we already know that there aren't enough funds inside our wallet). ### Test Coverage 1) Adds test coverage for the duplicated preset input selection bug that we have in v24. Where the wallet invalidly selects the preset inputs twice during the Coin Selection process. Which ends up with a "good" Coin Selection result that does not cover the total tx target amount. Which, alone, crashes the wallet due an insane fee. But.. to make it worst, adding the subtract fee from output functionality to this mix ends up with the wallet by-passing the "insane" fee assertion, decreasing the output amount to fulfill the insane fee, and.. sadly, broadcasting the tx to the network. 2) Adds test coverage for the `CoinsResult::Erase` method. ------------------------------------ TO DO: * [ ] Update [#26559 ](https://github.com/bitcoin/bitcoin/pull/26559) description. ACKs for top commit: achow101: ACK7362f8e5e2glozow: ACK7362f8e5e2, I assume there will be a followup PR to add coin selection sanity checks and we can discuss the best way to do that there. josibake: ACK [7362f8e](7362f8e5e2) Tree-SHA512: 37a6828ea10d8d36c8d5873ceede7c8bef72ae4c34bef21721fa9dad83ad6dba93711c3170a26ab6e05bdbc267bb17433da08ccb83b82956d05fb16090328cba
This commit is contained in:
@@ -110,6 +110,7 @@ class RawTransactionsTest(BitcoinTestFramework):
|
||||
self.generate(self.nodes[0], 121)
|
||||
|
||||
self.test_add_inputs_default_value()
|
||||
self.test_preset_inputs_selection()
|
||||
self.test_weight_calculation()
|
||||
self.test_change_position()
|
||||
self.test_simple()
|
||||
@@ -1203,6 +1204,50 @@ class RawTransactionsTest(BitcoinTestFramework):
|
||||
|
||||
self.nodes[2].unloadwallet("test_preset_inputs")
|
||||
|
||||
def test_preset_inputs_selection(self):
|
||||
self.log.info('Test wallet preset inputs are not double-counted or reused in coin selection')
|
||||
|
||||
# Create and fund the wallet with 4 UTXO of 5 BTC each (20 BTC total)
|
||||
self.nodes[2].createwallet("test_preset_inputs_selection")
|
||||
wallet = self.nodes[2].get_wallet_rpc("test_preset_inputs_selection")
|
||||
outputs = {}
|
||||
for _ in range(4):
|
||||
outputs[wallet.getnewaddress(address_type="bech32")] = 5
|
||||
self.nodes[0].sendmany("", outputs)
|
||||
self.generate(self.nodes[0], 1)
|
||||
|
||||
# Select the preset inputs
|
||||
coins = wallet.listunspent()
|
||||
preset_inputs = [coins[0], coins[1], coins[2]]
|
||||
|
||||
# Now let's create the tx creation options
|
||||
options = {
|
||||
"inputs": preset_inputs,
|
||||
"add_inputs": True, # automatically add coins from the wallet to fulfill the target
|
||||
"subtract_fee_from_outputs": [0], # deduct fee from first output
|
||||
"add_to_wallet": False
|
||||
}
|
||||
|
||||
# Attempt to send 29 BTC from a wallet that only has 20 BTC. The wallet should exclude
|
||||
# the preset inputs from the pool of available coins, realize that there is not enough
|
||||
# money to fund the 29 BTC payment, and fail with "Insufficient funds".
|
||||
#
|
||||
# Even with SFFO, the wallet can only afford to send 20 BTC.
|
||||
# If the wallet does not properly exclude preset inputs from the pool of available coins
|
||||
# prior to coin selection, it may create a transaction that does not fund the full payment
|
||||
# amount or, through SFFO, incorrectly reduce the recipient's amount by the difference
|
||||
# between the original target and the wrongly counted inputs (in this case 9 BTC)
|
||||
# so that the recipient's amount is no longer equal to the user's selected target of 29 BTC.
|
||||
|
||||
# First case, use 'subtract_fee_from_outputs = true'
|
||||
assert_raises_rpc_error(-4, "Insufficient funds", wallet.send, outputs=[{wallet.getnewaddress(address_type="bech32"): 29}], options=options)
|
||||
|
||||
# Second case, don't use 'subtract_fee_from_outputs'
|
||||
del options["subtract_fee_from_outputs"]
|
||||
assert_raises_rpc_error(-4, "Insufficient funds", wallet.send, outputs=[{wallet.getnewaddress(address_type="bech32"): 29}], options=options)
|
||||
|
||||
self.nodes[2].unloadwallet("test_preset_inputs_selection")
|
||||
|
||||
def test_weight_calculation(self):
|
||||
self.log.info("Test weight calculation with external inputs")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user