]> Untitled Git - bdk/commitdiff
fix(wallet): off-by-one error checking coinbase maturity in optional UTxOs
authornymius <155548262+nymius@users.noreply.github.com>
Wed, 12 Feb 2025 19:44:15 +0000 (16:44 -0300)
committernymius <155548262+nymius@users.noreply.github.com>
Wed, 12 Feb 2025 19:44:15 +0000 (16:44 -0300)
The `preselect_utxos` method has an off-by-one error that is 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.

The changes in this commit fix it by considering the maturity of the
coinbase output at the spending height and not the transaction creation
height, this means, a +1 at the considered height at the moment of
building the transaction.

crates/wallet/src/wallet/mod.rs
crates/wallet/src/wallet/tx_builder.rs
crates/wallet/tests/wallet.rs

index 3d60aa2ecbd4a0bc66396f3ffccf74c4ae794c5f..6e00b10945011f1bb59c6185bce18461c8f0b8a2 100644 (file)
@@ -2052,9 +2052,10 @@ impl Wallet {
                         match chain_position {
                             ChainPosition::Confirmed { anchor, .. } => {
                                 // https://github.com/bitcoin/bitcoin/blob/c5e67be03bb06a5d7885c55db1f016fbf2333fe3/src/validation.cpp#L373-L375
-                                spendable &= (current_height
-                                    .saturating_sub(anchor.block_id.height))
-                                    >= COINBASE_MATURITY;
+                                let spend_height = current_height + 1;
+                                let coin_age_at_spend_height =
+                                    spend_height.saturating_sub(anchor.block_id.height);
+                                spendable &= coin_age_at_spend_height >= COINBASE_MATURITY;
                             }
                             ChainPosition::Unconfirmed { .. } => spendable = false,
                         }
index 88d07cbb77bbf4455ed58e2d1f7aa3690776f2a8..da41c6b0050ea12650eaf500510f37cd8012e93e 100644 (file)
@@ -556,9 +556,9 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
     /// 1. Set the nLockTime for preventing fee sniping.
     ///    **Note**: This will be ignored if you manually specify a nlocktime using [`TxBuilder::nlocktime`].
     /// 2. Decide whether coinbase outputs are mature or not. If the coinbase outputs are not
-    ///    mature at `current_height`, we ignore them in the coin selection.
-    ///    If you want to create a transaction that spends immature coinbase inputs, manually
-    ///    add them using [`TxBuilder::add_utxos`].
+    ///    mature at spending height, which is `current_height` + 1, we ignore them in the coin
+    ///    selection. If you want to create a transaction that spends immature coinbase inputs,
+    ///    manually add them using [`TxBuilder::add_utxos`].
     ///
     /// In both cases, if you don't provide a current height, we use the last sync height.
     pub fn current_height(&mut self, height: u32) -> &mut Self {
index 174a628d2508755cbabf394c37e569c53c370974..f42f0bcd57b1c9868aff3629e798319f5975e0a7 100644 (file)
@@ -3875,8 +3875,16 @@ fn test_spend_coinbase() {
     };
     insert_anchor(&mut wallet, txid, anchor);
 
-    let not_yet_mature_time = confirmation_height + COINBASE_MATURITY - 1;
-    let maturity_time = confirmation_height + COINBASE_MATURITY;
+    // NOTE: A transaction spending an output coming from the coinbase tx at height h, is eligible
+    // to be included in block h + [100 = COINBASE_MATURITY] or higher.
+    // Tx elibible to be included in the next block will be accepted in the mempool, used in block
+    // templates and relayed on the network.
+    // Miners may include such tx in a block when their chaintip is at h + [99 = COINBASE_MATURITY - 1].
+    // This means these coins are available for selection at height h + 99.
+    //
+    // By https://bitcoin.stackexchange.com/a/119017
+    let not_yet_mature_time = confirmation_height + COINBASE_MATURITY - 2;
+    let maturity_time = confirmation_height + COINBASE_MATURITY - 1;
 
     let balance = wallet.balance();
     assert_eq!(