Avoid using immature coinbase inputs

Fixes #413
This commit is contained in:
Daniela Brozzoni 2022-05-25 18:56:50 +01:00
parent 0e0d5a0e95
commit e85aa247cb
No known key found for this signature in database
GPG Key ID: 7DE4F1FDCED0AB87
2 changed files with 115 additions and 19 deletions

View File

@ -73,6 +73,7 @@ use crate::testutils;
use crate::types::*; use crate::types::*;
const CACHE_ADDR_BATCH_SIZE: u32 = 100; const CACHE_ADDR_BATCH_SIZE: u32 = 100;
const COINBASE_MATURITY: u32 = 100;
/// A Bitcoin wallet /// A Bitcoin wallet
/// ///
@ -765,6 +766,7 @@ where
params.drain_wallet, params.drain_wallet,
params.manually_selected_only, params.manually_selected_only,
params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee
current_height,
)?; )?;
let coin_selection = coin_selection.coin_select( let coin_selection = coin_selection.coin_select(
@ -1335,6 +1337,7 @@ where
/// Given the options returns the list of utxos that must be used to form the /// Given the options returns the list of utxos that must be used to form the
/// transaction and any further that may be used if needed. /// transaction and any further that may be used if needed.
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
#[allow(clippy::too_many_arguments)]
fn preselect_utxos( fn preselect_utxos(
&self, &self,
change_policy: tx_builder::ChangeSpendPolicy, change_policy: tx_builder::ChangeSpendPolicy,
@ -1343,6 +1346,7 @@ where
must_use_all_available: bool, must_use_all_available: bool,
manual_only: bool, manual_only: bool,
must_only_use_confirmed_tx: bool, must_only_use_confirmed_tx: bool,
current_height: Option<u32>,
) -> Result<(Vec<WeightedUtxo>, Vec<WeightedUtxo>), Error> { ) -> Result<(Vec<WeightedUtxo>, Vec<WeightedUtxo>), Error> {
// must_spend <- manually selected utxos // must_spend <- manually selected utxos
// may_spend <- all other available utxos // may_spend <- all other available utxos
@ -1361,23 +1365,44 @@ where
return Ok((must_spend, vec![])); return Ok((must_spend, vec![]));
} }
let satisfies_confirmed = match must_only_use_confirmed_tx { let database = self.database.borrow();
true => { let satisfies_confirmed = may_spend
let database = self.database.borrow(); .iter()
may_spend .map(|u| {
.iter() database
.map(|u| { .get_tx(&u.0.outpoint.txid, true)
database .map(|tx| match tx {
.get_tx(&u.0.outpoint.txid, true) // We don't have the tx in the db for some reason,
.map(|tx| match tx { // so we can't know for sure if it's mature or not.
None => false, // We prefer not to spend it.
Some(tx) => tx.confirmation_time.is_some(), None => false,
}) Some(tx) => {
// Whether the UTXO is mature and, if needed, confirmed
let mut spendable = true;
if must_only_use_confirmed_tx && tx.confirmation_time.is_none() {
return false;
}
if tx
.transaction
.expect("We specifically ask for the transaction above")
.is_coin_base()
{
if let Some(current_height) = current_height {
match &tx.confirmation_time {
Some(t) => {
// https://github.com/bitcoin/bitcoin/blob/c5e67be03bb06a5d7885c55db1f016fbf2333fe3/src/validation.cpp#L373-L375
spendable &= (current_height.saturating_sub(t.height))
>= COINBASE_MATURITY;
}
None => spendable = false,
}
}
}
spendable
}
}) })
.collect::<Result<Vec<_>, _>>()? })
} .collect::<Result<Vec<_>, _>>()?;
false => vec![true; may_spend.len()],
};
let mut i = 0; let mut i = 0;
may_spend.retain(|u| { may_spend.retain(|u| {
@ -4643,4 +4668,70 @@ pub(crate) mod test {
"The signature should have been made with the right sighash" "The signature should have been made with the right sighash"
); );
} }
#[test]
fn test_spend_coinbase() {
let descriptors = testutils!(@descriptors (get_test_wpkh()));
let wallet = Wallet::new(
&descriptors.0,
None,
Network::Regtest,
AnyDatabase::Memory(MemoryDatabase::new()),
)
.unwrap();
let confirmation_time = 5;
crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 0)),
Some(confirmation_time),
(@coinbase true)
);
let not_yet_mature_time = confirmation_time + COINBASE_MATURITY - 1;
let maturity_time = confirmation_time + COINBASE_MATURITY;
// The balance is nonzero, even if we can't spend anything
// FIXME: we should differentiate the balance between immature,
// trusted, untrusted_pending
// See https://github.com/bitcoindevkit/bdk/issues/238
let balance = wallet.get_balance().unwrap();
assert!(balance != 0);
// We try to create a transaction, only to notice that all
// our funds are unspendable
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), balance / 2)
.set_current_height(confirmation_time);
assert!(matches!(
builder.finish().unwrap_err(),
Error::InsufficientFunds {
needed: _,
available: 0
}
));
// Still unspendable...
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), balance / 2)
.set_current_height(not_yet_mature_time);
assert!(matches!(
builder.finish().unwrap_err(),
Error::InsufficientFunds {
needed: _,
available: 0
}
));
// ...Now the coinbase is mature :)
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), balance / 2)
.set_current_height(maturity_time);
builder.finish().unwrap();
}
} }

View File

@ -547,10 +547,15 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
/// Set the current blockchain height. /// Set the current blockchain height.
/// ///
/// This will be used to set the nLockTime for preventing fee sniping. If the current height is /// This will be used to:
/// not provided, the last sync height will be used instead. /// 1. Set the nLockTime for preventing fee sniping.
///
/// **Note**: This will be ignored if you manually specify a nlocktime using [`TxBuilder::nlocktime`]. /// **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`].
///
/// In both cases, if you don't provide a current height, we use the last sync height.
pub fn set_current_height(&mut self, height: u32) -> &mut Self { pub fn set_current_height(&mut self, height: u32) -> &mut Self {
self.params.current_height = Some(height); self.params.current_height = Some(height);
self self