Skip to content

Commit e85aa24

Browse files
Avoid using immature coinbase inputs
Fixes #413
1 parent 0e0d5a0 commit e85aa24

File tree

2 files changed

+115
-19
lines changed

2 files changed

+115
-19
lines changed

Diff for: src/wallet/mod.rs

+107-16
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ use crate::testutils;
7373
use crate::types::*;
7474

7575
const CACHE_ADDR_BATCH_SIZE: u32 = 100;
76+
const COINBASE_MATURITY: u32 = 100;
7677

7778
/// A Bitcoin wallet
7879
///
@@ -765,6 +766,7 @@ where
765766
params.drain_wallet,
766767
params.manually_selected_only,
767768
params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee
769+
current_height,
768770
)?;
769771

770772
let coin_selection = coin_selection.coin_select(
@@ -1335,6 +1337,7 @@ where
13351337
/// Given the options returns the list of utxos that must be used to form the
13361338
/// transaction and any further that may be used if needed.
13371339
#[allow(clippy::type_complexity)]
1340+
#[allow(clippy::too_many_arguments)]
13381341
fn preselect_utxos(
13391342
&self,
13401343
change_policy: tx_builder::ChangeSpendPolicy,
@@ -1343,6 +1346,7 @@ where
13431346
must_use_all_available: bool,
13441347
manual_only: bool,
13451348
must_only_use_confirmed_tx: bool,
1349+
current_height: Option<u32>,
13461350
) -> Result<(Vec<WeightedUtxo>, Vec<WeightedUtxo>), Error> {
13471351
// must_spend <- manually selected utxos
13481352
// may_spend <- all other available utxos
@@ -1361,23 +1365,44 @@ where
13611365
return Ok((must_spend, vec![]));
13621366
}
13631367

1364-
let satisfies_confirmed = match must_only_use_confirmed_tx {
1365-
true => {
1366-
let database = self.database.borrow();
1367-
may_spend
1368-
.iter()
1369-
.map(|u| {
1370-
database
1371-
.get_tx(&u.0.outpoint.txid, true)
1372-
.map(|tx| match tx {
1373-
None => false,
1374-
Some(tx) => tx.confirmation_time.is_some(),
1375-
})
1368+
let database = self.database.borrow();
1369+
let satisfies_confirmed = may_spend
1370+
.iter()
1371+
.map(|u| {
1372+
database
1373+
.get_tx(&u.0.outpoint.txid, true)
1374+
.map(|tx| match tx {
1375+
// We don't have the tx in the db for some reason,
1376+
// so we can't know for sure if it's mature or not.
1377+
// We prefer not to spend it.
1378+
None => false,
1379+
Some(tx) => {
1380+
// Whether the UTXO is mature and, if needed, confirmed
1381+
let mut spendable = true;
1382+
if must_only_use_confirmed_tx && tx.confirmation_time.is_none() {
1383+
return false;
1384+
}
1385+
if tx
1386+
.transaction
1387+
.expect("We specifically ask for the transaction above")
1388+
.is_coin_base()
1389+
{
1390+
if let Some(current_height) = current_height {
1391+
match &tx.confirmation_time {
1392+
Some(t) => {
1393+
// https://github.com/bitcoin/bitcoin/blob/c5e67be03bb06a5d7885c55db1f016fbf2333fe3/src/validation.cpp#L373-L375
1394+
spendable &= (current_height.saturating_sub(t.height))
1395+
>= COINBASE_MATURITY;
1396+
}
1397+
None => spendable = false,
1398+
}
1399+
}
1400+
}
1401+
spendable
1402+
}
13761403
})
1377-
.collect::<Result<Vec<_>, _>>()?
1378-
}
1379-
false => vec![true; may_spend.len()],
1380-
};
1404+
})
1405+
.collect::<Result<Vec<_>, _>>()?;
13811406

13821407
let mut i = 0;
13831408
may_spend.retain(|u| {
@@ -4643,4 +4668,70 @@ pub(crate) mod test {
46434668
"The signature should have been made with the right sighash"
46444669
);
46454670
}
4671+
4672+
#[test]
4673+
fn test_spend_coinbase() {
4674+
let descriptors = testutils!(@descriptors (get_test_wpkh()));
4675+
let wallet = Wallet::new(
4676+
&descriptors.0,
4677+
None,
4678+
Network::Regtest,
4679+
AnyDatabase::Memory(MemoryDatabase::new()),
4680+
)
4681+
.unwrap();
4682+
4683+
let confirmation_time = 5;
4684+
4685+
crate::populate_test_db!(
4686+
wallet.database.borrow_mut(),
4687+
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 0)),
4688+
Some(confirmation_time),
4689+
(@coinbase true)
4690+
);
4691+
4692+
let not_yet_mature_time = confirmation_time + COINBASE_MATURITY - 1;
4693+
let maturity_time = confirmation_time + COINBASE_MATURITY;
4694+
4695+
// The balance is nonzero, even if we can't spend anything
4696+
// FIXME: we should differentiate the balance between immature,
4697+
// trusted, untrusted_pending
4698+
// See https://github.com/bitcoindevkit/bdk/issues/238
4699+
let balance = wallet.get_balance().unwrap();
4700+
assert!(balance != 0);
4701+
4702+
// We try to create a transaction, only to notice that all
4703+
// our funds are unspendable
4704+
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
4705+
let mut builder = wallet.build_tx();
4706+
builder
4707+
.add_recipient(addr.script_pubkey(), balance / 2)
4708+
.set_current_height(confirmation_time);
4709+
assert!(matches!(
4710+
builder.finish().unwrap_err(),
4711+
Error::InsufficientFunds {
4712+
needed: _,
4713+
available: 0
4714+
}
4715+
));
4716+
4717+
// Still unspendable...
4718+
let mut builder = wallet.build_tx();
4719+
builder
4720+
.add_recipient(addr.script_pubkey(), balance / 2)
4721+
.set_current_height(not_yet_mature_time);
4722+
assert!(matches!(
4723+
builder.finish().unwrap_err(),
4724+
Error::InsufficientFunds {
4725+
needed: _,
4726+
available: 0
4727+
}
4728+
));
4729+
4730+
// ...Now the coinbase is mature :)
4731+
let mut builder = wallet.build_tx();
4732+
builder
4733+
.add_recipient(addr.script_pubkey(), balance / 2)
4734+
.set_current_height(maturity_time);
4735+
builder.finish().unwrap();
4736+
}
46464737
}

Diff for: src/wallet/tx_builder.rs

+8-3
Original file line numberDiff line numberDiff line change
@@ -547,10 +547,15 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
547547

548548
/// Set the current blockchain height.
549549
///
550-
/// This will be used to set the nLockTime for preventing fee sniping. If the current height is
551-
/// not provided, the last sync height will be used instead.
552-
///
550+
/// This will be used to:
551+
/// 1. Set the nLockTime for preventing fee sniping.
553552
/// **Note**: This will be ignored if you manually specify a nlocktime using [`TxBuilder::nlocktime`].
553+
/// 2. Decide whether coinbase outputs are mature or not. If the coinbase outputs are not
554+
/// mature at `current_height`, we ignore them in the coin selection.
555+
/// If you want to create a transaction that spends immature coinbase inputs, manually
556+
/// add them using [`TxBuilder::add_utxos`].
557+
///
558+
/// In both cases, if you don't provide a current height, we use the last sync height.
554559
pub fn set_current_height(&mut self, height: u32) -> &mut Self {
555560
self.params.current_height = Some(height);
556561
self

0 commit comments

Comments
 (0)