Skip to content

Commit ed75952

Browse files
committed
spend: a temporary partial fix for LowestFee
This is a temporary partial fix for bitcoindevkit/coin-select#6 that should be reverted once the upstream fix has been made. When calculating the score, the excess should be added to changeless solutions instead of those with change. Given a solution has been found, this fix adds or removes the excess to its incorrectly calculated score as required so that two changeless solutions can be differentiated if one has higher excess (and therefore pays a higher fee). Note that the `bound` function is also affected by this bug, which could mean some branches are not considered when running BnB, but at least this fix will mean the score for those solutions that are found is correct.
1 parent 3e0f82a commit ed75952

File tree

2 files changed

+43
-2
lines changed

2 files changed

+43
-2
lines changed

src/spend.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,27 @@ where
201201
if drain.is_none() && self.must_have_change {
202202
None
203203
} else {
204-
self.lowest_fee.score(cs)
204+
// This is a temporary partial fix for https://github.com/bitcoindevkit/coin-select/issues/6
205+
// until it has been fixed upstream.
206+
// TODO: Revert this change once upstream fix has been made.
207+
// When calculating the score, the excess should be added to changeless solutions instead of
208+
// those with change.
209+
// Given a solution has been found, this fix adds or removes the excess to its incorrectly
210+
// calculated score as required so that two changeless solutions can be differentiated
211+
// if one has higher excess (and therefore pays a higher fee).
212+
// Note that the `bound` function is also affected by this bug, which could mean some branches
213+
// are not considered when running BnB, but at least this fix will mean the score for those
214+
// solutions that are found is correct.
215+
self.lowest_fee.score(cs).map(|score| {
216+
// See https://github.com/bitcoindevkit/coin-select/blob/29b187f5509a01ba125a0354f6711e317bb5522a/src/metrics/lowest_fee.rs#L35-L45
217+
assert!(cs.selected_value() >= self.lowest_fee.target.value);
218+
let excess = (cs.selected_value() - self.lowest_fee.target.value) as f32;
219+
bdk_coin_select::float::Ordf32(if drain.is_none() {
220+
score.0 + excess
221+
} else {
222+
score.0 - excess
223+
})
224+
})
205225
}
206226
}
207227

tests/test_spend.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from fixtures import *
2-
from test_framework.serializations import PSBT
2+
from test_framework.serializations import (
3+
PSBT,
4+
PSBT_IN_NON_WITNESS_UTXO,
5+
)
36
from test_framework.utils import wait_for, COIN, RpcError
47

58

@@ -332,6 +335,24 @@ def test_coin_selection(lianad, bitcoind):
332335
assert auto_psbt.tx.vout[1].nValue == manual_psbt.tx.vout[1].nValue
333336

334337

338+
def test_coin_selection_changeless(lianad, bitcoind):
339+
"""We choose the changeless solution with lowest fee."""
340+
# Get two coins with similar amounts.
341+
txid_a = bitcoind.rpc.sendtoaddress(lianad.rpc.getnewaddress()["address"], 0.00031)
342+
txid_b = bitcoind.rpc.sendtoaddress(lianad.rpc.getnewaddress()["address"], 0.00032)
343+
bitcoind.generate_block(1, wait_for_mempool=[txid_a, txid_b])
344+
wait_for(lambda: len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 2)
345+
# Send an amount that can be paid by just one of our coins.
346+
res = lianad.rpc.createspend({bitcoind.rpc.getnewaddress(): 30800}, [], 1)
347+
psbt = PSBT.from_base64(res["psbt"])
348+
# Only one input needed.
349+
assert len(psbt.i) == 1
350+
# Coin A is used as input.
351+
assert [psbt_in.map[PSBT_IN_NON_WITNESS_UTXO] for psbt_in in psbt.i][
352+
0
353+
] == bytes.fromhex(bitcoind.rpc.gettransaction(txid_a)["hex"])
354+
355+
335356
def test_sweep(lianad, bitcoind):
336357
"""
337358
Test we can leverage the change_address parameter to partially or completely sweep

0 commit comments

Comments
 (0)