Skip to content

Commit 905c8dd

Browse files
committed
fix(wallet): off-by-one error checking coinbase maturity in optional UTxOs
The `preselect_utxos` method (now `filter_utxos`) had an off-by-one error that was making the selection of optional UTxOs too restrictive, by requiring the coinbase outputs to surpass or equal coinbase maturity time at the current height of the selection, and not in the block in which the transaction may be included in the blockchain (be spent), probably, the next one. The bug is still in `filter_utxos`. The changes in this commit fix it by making use of the correctly defined `FullTxOut<A>::is_mature` method, which test positively a UTxO as mature if it is elegible for inclusion in the next mined block.
1 parent 9c6ce71 commit 905c8dd

2 files changed

Lines changed: 26 additions & 22 deletions

File tree

crates/wallet/src/wallet/mod.rs

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ use bdk_chain::{
3636
use bitcoin::{
3737
absolute,
3838
consensus::encode::serialize,
39-
constants::{genesis_block, COINBASE_MATURITY},
39+
constants::genesis_block,
4040
psbt,
4141
secp256k1::Secp256k1,
4242
sighash::{EcdsaSighashType, TapSighashType},
@@ -2010,9 +2010,22 @@ impl Wallet {
20102010
vec![]
20112011
// only process optional UTxOs if manually_selected_only is false
20122012
} else {
2013-
self
2013+
self.indexed_graph
2014+
.graph()
20142015
// get all unspent UTxOs from wallet
2015-
.list_unspent()
2016+
// NOTE: the UTxOs returned by the following method already belong to wallet as the
2017+
// call chain uses get_tx_node infallibly
2018+
.filter_chain_unspents(
2019+
&self.chain,
2020+
self.chain.tip().block_id(),
2021+
self.indexed_graph.index.outpoints().iter().cloned(),
2022+
)
2023+
// only create LocalOutput if UTxO is mature
2024+
.filter_map(move |((k, i), full_txo)| {
2025+
full_txo
2026+
.is_mature(current_height)
2027+
.then(|| new_local_utxo(k, i, full_txo))
2028+
})
20162029
// only process UTxOs not selected manually, they will be considered later in the chain
20172030
// NOTE: this avoid UTxOs in both required and optional list
20182031
.filter(|may_spend| !params.utxos.contains(may_spend))
@@ -2027,23 +2040,6 @@ impl Wallet {
20272040
.filter(|local_output| {
20282041
params.bumping_fee.is_none() || local_output.chain_position.is_confirmed()
20292042
})
2030-
// only use UTxOs we posess and are mature if the origin tx is coinbase
2031-
.filter(move |local_output| {
2032-
self.indexed_graph
2033-
.graph()
2034-
.get_tx(local_output.outpoint.txid)
2035-
.is_some_and(|tx| {
2036-
!tx.is_coinbase()
2037-
|| (local_output.chain_position.is_confirmed()
2038-
&& local_output
2039-
.chain_position
2040-
.confirmation_height_upper_bound()
2041-
.is_some_and(|local_output_height| {
2042-
current_height.saturating_sub(local_output_height)
2043-
>= COINBASE_MATURITY
2044-
}))
2045-
})
2046-
})
20472043
.map(|utxo| WeightedUtxo {
20482044
satisfaction_weight: self
20492045
.public_descriptor(utxo.keychain)

crates/wallet/tests/wallet.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3875,8 +3875,16 @@ fn test_spend_coinbase() {
38753875
};
38763876
insert_anchor(&mut wallet, txid, anchor);
38773877

3878-
let not_yet_mature_time = confirmation_height + COINBASE_MATURITY - 1;
3879-
let maturity_time = confirmation_height + COINBASE_MATURITY;
3878+
// NOTE: A transaction spending an output coming from the coinbase tx at height h, is eligible
3879+
// to be included in block h + [100 = COINBASE_MATURITY] or higher.
3880+
// Tx elibible to be included in the next block will be accepted in the mempool, used in block
3881+
// templates and relayed on the network.
3882+
// Miners may include such tx in a block when their chaintip is at h + [99 = COINBASE_MATURITY - 1].
3883+
// This means these coins are available for selection at height h + 99.
3884+
//
3885+
// By https://bitcoin.stackexchange.com/a/119017
3886+
let not_yet_mature_time = confirmation_height + COINBASE_MATURITY - 2;
3887+
let maturity_time = confirmation_height + COINBASE_MATURITY - 1;
38803888

38813889
let balance = wallet.balance();
38823890
assert_eq!(

0 commit comments

Comments
 (0)