use crate::{bitcoin::MempoolEntry, descriptors};
use std::{
collections::{BTreeMap, HashMap},
convert::TryInto,
fmt,
};
pub use bdk_coin_select::InsufficientFunds;
use bdk_coin_select::{
change_policy, metrics::LowestFee, Candidate, CoinSelector, DrainWeights, FeeRate, Target,
TXIN_BASE_WEIGHT,
};
use miniscript::bitcoin::{
self,
absolute::{Height, LockTime},
bip32,
constants::WITNESS_SCALE_FACTOR,
psbt::{Input as PsbtIn, Output as PsbtOut, Psbt},
secp256k1,
};
use serde::{Deserialize, Serialize};
pub const DUST_OUTPUT_SATS: u64 = 5_000;
pub const LONG_TERM_FEERATE_VB: f32 = 10.0;
pub const MAX_FEE: bitcoin::Amount = bitcoin::Amount::ONE_BTC;
pub const MAX_FEERATE: u64 = 1_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InsaneFeeInfo {
NegativeFee,
InvalidFeerate,
TooHighFee(u64),
TooHighFeerate(u64),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SpendCreationError {
InvalidFeerate( u64),
InvalidOutputValue(bitcoin::Amount),
InsaneFees(InsaneFeeInfo),
SanityCheckFailure(Psbt),
FetchingTransaction(bitcoin::OutPoint),
CoinSelection(InsufficientFunds),
}
impl fmt::Display for SpendCreationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::InvalidFeerate(sats_vb) => write!(f, "Invalid feerate: {} sats/vb.", sats_vb),
Self::InvalidOutputValue(amount) => write!(f, "Invalid output value '{}'.", amount),
Self::InsaneFees(info) => write!(
f,
"We assume transactions with a fee larger than {} or a feerate larger than {} sats/vb are a mistake. \
The created transaction {}.",
MAX_FEE,
MAX_FEERATE,
match info {
InsaneFeeInfo::NegativeFee => "would have a negative fee".to_string(),
InsaneFeeInfo::TooHighFee(f) => format!("{} sats in fees", f),
InsaneFeeInfo::InvalidFeerate => "would have an invalid feerate".to_string(),
InsaneFeeInfo::TooHighFeerate(r) => format!("has a feerate of {} sats/vb", r),
},
),
Self::FetchingTransaction(op) => {
write!(f, "Could not fetch transaction for coin {}", op)
}
Self::CoinSelection(e) => write!(f, "Coin selection error: '{}'", e),
Self::SanityCheckFailure(psbt) => write!(
f,
"BUG! Please report this. Failed sanity checks for PSBT '{}'.",
psbt
),
}
}
}
impl std::error::Error for SpendCreationError {}
fn check_output_value(value: bitcoin::Amount) -> Result<(), SpendCreationError> {
if value > bitcoin::Amount::MAX_MONEY || value.to_sat() < DUST_OUTPUT_SATS {
Err(SpendCreationError::InvalidOutputValue(value))
} else {
Ok(())
}
}
fn sanity_check_psbt(
spent_desc: &descriptors::LianaDescriptor,
psbt: &Psbt,
) -> Result<(), SpendCreationError> {
let tx = &psbt.unsigned_tx;
if psbt.inputs.len() != tx.input.len()
|| psbt.outputs.len() != tx.output.len()
|| tx.output.is_empty()
{
return Err(SpendCreationError::SanityCheckFailure(psbt.clone()));
}
let mut value_in = 0;
for psbtin in psbt.inputs.iter() {
if psbtin.bip32_derivation.is_empty() && psbtin.tap_key_origins.is_empty() {
return Err(SpendCreationError::SanityCheckFailure(psbt.clone()));
}
value_in += psbtin
.witness_utxo
.as_ref()
.ok_or_else(|| SpendCreationError::SanityCheckFailure(psbt.clone()))?
.value
.to_sat();
}
let value_out: u64 = tx.output.iter().map(|o| o.value.to_sat()).sum();
let abs_fee = value_in
.checked_sub(value_out)
.ok_or(SpendCreationError::InsaneFees(InsaneFeeInfo::NegativeFee))?;
if abs_fee > MAX_FEE.to_sat() {
return Err(SpendCreationError::InsaneFees(InsaneFeeInfo::TooHighFee(
abs_fee,
)));
}
let tx_vb = spent_desc.unsigned_tx_max_vbytes(tx);
let feerate_sats_vb = abs_fee
.checked_div(tx_vb)
.ok_or(SpendCreationError::InsaneFees(
InsaneFeeInfo::InvalidFeerate,
))?;
if !(1..=MAX_FEERATE).contains(&feerate_sats_vb) {
return Err(SpendCreationError::InsaneFees(
InsaneFeeInfo::TooHighFeerate(feerate_sats_vb),
));
}
for txo in psbt.unsigned_tx.output.iter() {
if txo.value < txo.script_pubkey.dust_value() {
return Err(SpendCreationError::SanityCheckFailure(psbt.clone()));
}
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AncestorInfo {
pub vsize: u32,
pub fee: u32,
}
impl From<MempoolEntry> for AncestorInfo {
fn from(entry: MempoolEntry) -> Self {
Self {
vsize: entry
.ancestor_vsize
.try_into()
.expect("vsize must fit in a u32"),
fee: entry
.fees
.ancestor
.to_sat()
.try_into()
.expect("fee in sats should fit in a u32"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CandidateCoin {
pub outpoint: bitcoin::OutPoint,
pub amount: bitcoin::Amount,
pub deriv_index: bip32::ChildNumber,
pub is_change: bool,
pub must_select: bool,
pub sequence: Option<bitcoin::Sequence>,
pub ancestor_info: Option<AncestorInfo>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CoinSelectionRes {
pub selected: Vec<CandidateCoin>,
pub change_amount: bitcoin::Amount,
pub max_change_amount: bitcoin::Amount,
pub fee_for_ancestors: bitcoin::Amount,
}
struct LowestFeeChangeCondition<'c, C> {
pub lowest_fee: LowestFee<'c, C>,
pub must_have_change: bool,
}
impl<'c, C> bdk_coin_select::BnbMetric for LowestFeeChangeCondition<'c, C>
where
for<'a, 'b> C: Fn(&'b CoinSelector<'a>, Target) -> bdk_coin_select::Drain,
{
fn score(&mut self, cs: &CoinSelector<'_>) -> Option<bdk_coin_select::float::Ordf32> {
let drain = (self.lowest_fee.change_policy)(cs, self.lowest_fee.target);
if drain.is_none() && self.must_have_change {
None
} else {
self.lowest_fee.score(cs).map(|score| {
assert!(cs.selected_value() >= self.lowest_fee.target.value);
let excess = (cs.selected_value() - self.lowest_fee.target.value) as f32;
bdk_coin_select::float::Ordf32(if drain.is_none() {
score.0 + excess
} else {
score.0 - excess
})
})
}
}
fn bound(&mut self, cs: &CoinSelector<'_>) -> Option<bdk_coin_select::float::Ordf32> {
self.lowest_fee.bound(cs)
}
fn requires_ordering_by_descending_value_pwu(&self) -> bool {
self.lowest_fee.requires_ordering_by_descending_value_pwu()
}
}
fn select_coins_for_spend(
candidate_coins: &[CandidateCoin],
base_tx: bitcoin::Transaction,
change_txo: bitcoin::TxOut,
feerate_vb: f32,
min_fee: u64,
max_sat_weight: u32,
must_have_change: bool,
) -> Result<CoinSelectionRes, InsufficientFunds> {
let out_value_nochange = base_tx.output.iter().map(|o| o.value.to_sat()).sum();
let mut base_weight: u32 = base_tx
.weight()
.to_wu()
.try_into()
.expect("Transaction weight must fit in u32");
if base_tx.input.is_empty() {
base_weight = base_weight.saturating_sub(2);
}
let max_input_weight = TXIN_BASE_WEIGHT + max_sat_weight;
let feerate_vb_u32 = feerate_vb.ceil() as u32;
let witness_factor: u32 = WITNESS_SCALE_FACTOR
.try_into()
.expect("scale factor must fit in u32");
let mut added_weights = HashMap::<bitcoin::OutPoint, u32>::with_capacity(candidate_coins.len());
let candidates: Vec<Candidate> = candidate_coins
.iter()
.map(|cand| Candidate {
input_count: 1,
value: cand.amount.to_sat(),
weight: {
let extra = cand
.ancestor_info
.map(|info| {
let ancestor_vsize_at_feerate: u32 = info
.fee
.checked_div(feerate_vb_u32)
.expect("feerate is greater than zero");
info.vsize
.saturating_sub(ancestor_vsize_at_feerate)
.checked_mul(witness_factor)
.expect("weight difference must fit in u32")
})
.unwrap_or(0);
assert!(added_weights.insert(cand.outpoint, extra).is_none());
max_input_weight
.checked_add(extra)
.expect("effective weight must fit in u32")
},
is_segwit: true, })
.collect();
let mut selector = CoinSelector::new(&candidates, base_weight);
for (i, cand) in candidate_coins.iter().enumerate() {
if cand.must_select {
selector.select(i);
}
}
let long_term_feerate = FeeRate::from_sat_per_vb(LONG_TERM_FEERATE_VB);
let drain_weights = DrainWeights {
output_weight: {
let nochange_weight = base_tx.weight().to_wu();
let mut tx_with_change = base_tx;
tx_with_change.output.push(change_txo);
tx_with_change
.weight()
.to_wu()
.checked_sub(nochange_weight)
.expect("base_weight can't be larger")
.try_into()
.expect("tx size must always fit in u32")
},
spend_weight: max_input_weight,
};
let change_policy =
change_policy::min_value_and_waste(drain_weights, DUST_OUTPUT_SATS, long_term_feerate);
let feerate = FeeRate::from_sat_per_vb(feerate_vb);
let target = Target {
value: out_value_nochange,
feerate,
min_fee,
};
let lowest_fee = LowestFee {
target,
long_term_feerate,
change_policy: &change_policy,
};
let lowest_fee_change_cond = LowestFeeChangeCondition {
lowest_fee,
must_have_change,
};
let bnb_rounds = match candidate_coins.len() {
i if i >= 500 => 1_000,
i if i >= 100 => 10_000,
_ => 100_000,
};
#[cfg(debug)]
let bnb_rounds = bnb_rounds / 1_000;
if let Err(e) = selector.run_bnb(lowest_fee_change_cond, bnb_rounds) {
log::debug!(
"Coin selection error: '{}'. Selecting coins by descending value per weight unit...",
e.to_string()
);
selector.sort_candidates_by_descending_value_pwu();
loop {
let drain = change_policy(&selector, target);
if selector.is_target_met(target, drain) && (drain.is_some() || !must_have_change) {
break;
}
if !selector.select_next() {
let drain = if must_have_change {
bdk_coin_select::Drain {
weights: drain_weights,
value: DUST_OUTPUT_SATS,
}
} else {
drain
};
let missing = selector.excess(target, drain).unsigned_abs();
return Err(InsufficientFunds { missing });
}
}
}
let drain = change_policy(&selector, target);
let change_amount = bitcoin::Amount::from_sat(drain.value);
let drain_novalue = bdk_coin_select::Drain {
weights: drain_weights,
value: 0,
};
let max_change_amount = bitcoin::Amount::from_sat(
selector
.excess(target, drain_novalue)
.max(0) .try_into()
.expect("value is non-negative"),
);
let mut total_added_weight: u32 = 0;
let selected = selector
.selected_indices()
.iter()
.map(|i| candidate_coins[*i])
.inspect(|cand| {
total_added_weight = total_added_weight
.checked_add(
*added_weights
.get(&cand.outpoint)
.expect("contains added weight for all candidates"),
)
.expect("should fit in u32")
})
.collect();
let fee_for_ancestors =
bitcoin::Amount::from_sat(((total_added_weight as f32) * feerate.spwu()).ceil() as u64);
Ok(CoinSelectionRes {
selected,
change_amount,
max_change_amount,
fee_for_ancestors,
})
}
fn derived_desc(
secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
desc: &descriptors::LianaDescriptor,
coin: &CandidateCoin,
) -> descriptors::DerivedSinglePathLianaDesc {
let desc = if coin.is_change {
desc.change_descriptor()
} else {
desc.receive_descriptor()
};
desc.derive(coin.deriv_index, secp)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AddrInfo {
pub index: bip32::ChildNumber,
pub is_change: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpendOutputAddress {
pub addr: bitcoin::Address,
pub info: Option<AddrInfo>,
}
pub trait TxGetter {
fn get_tx(&mut self, txid: &bitcoin::Txid) -> Option<bitcoin::Transaction>;
}
pub enum SpendTxFees {
Regular(u64),
Rbf(u64, u64),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum CreateSpendWarning {
ChangeAddedToFee(u64),
AdditionalFeeForAncestors(u64),
}
impl fmt::Display for CreateSpendWarning {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
CreateSpendWarning::ChangeAddedToFee(amt) => write!(
f,
"Dust UTXO. The minimal change output allowed by Liana is {} sats. \
Instead of creating a change of {} sat{}, it was added to the \
transaction fee. Select a larger input to avoid this from happening.",
DUST_OUTPUT_SATS,
amt,
if *amt > 1 { "s" } else { "" },
),
CreateSpendWarning::AdditionalFeeForAncestors(amt) => write!(
f,
"CPFP: an unconfirmed input was selected. The current transaction fee \
was increased by {} sat{} to make the average feerate of both the input \
and current transaction equal to the selected feerate.",
amt,
if *amt > 1 { "s" } else { "" },
),
}
}
}
pub struct CreateSpendRes {
pub psbt: Psbt,
pub has_change: bool,
pub warnings: Vec<CreateSpendWarning>,
}
pub fn create_spend(
main_descriptor: &descriptors::LianaDescriptor,
secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
tx_getter: &mut impl TxGetter,
destinations: &[(SpendOutputAddress, bitcoin::Amount)],
candidate_coins: &[CandidateCoin],
fees: SpendTxFees,
change_addr: SpendOutputAddress,
) -> Result<CreateSpendRes, SpendCreationError> {
let mut warnings = Vec::new();
let (feerate_vb, min_fee) = match fees {
SpendTxFees::Regular(feerate) => (feerate, 0),
SpendTxFees::Rbf(feerate, fee) => (feerate, fee),
};
let is_self_send = destinations.is_empty();
if feerate_vb < 1 {
return Err(SpendCreationError::InvalidFeerate(feerate_vb));
}
let mut tx = bitcoin::Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: LockTime::Blocks(Height::ZERO), input: Vec::with_capacity(candidate_coins.iter().filter(|c| c.must_select).count()),
output: Vec::with_capacity(destinations.len()),
};
let mut psbt_outs = Vec::with_capacity(destinations.len());
for (address, amount) in destinations {
check_output_value(*amount)?;
tx.output.push(bitcoin::TxOut {
value: *amount,
script_pubkey: address.addr.script_pubkey(),
});
let mut psbt_out = PsbtOut::default();
if let Some(AddrInfo { index, is_change }) = address.info {
let desc = if is_change {
main_descriptor.change_descriptor()
} else {
main_descriptor.receive_descriptor()
};
desc.derive(index, secp)
.update_change_psbt_out(&mut psbt_out)
}
psbt_outs.push(psbt_out);
}
assert_eq!(tx.output.is_empty(), is_self_send);
let mut change_txo = bitcoin::TxOut {
value: bitcoin::Amount::MAX,
script_pubkey: change_addr.addr.script_pubkey(),
};
let CoinSelectionRes {
selected,
change_amount,
max_change_amount,
fee_for_ancestors,
} = {
assert!(tx.input.is_empty());
assert_eq!(tx.output.len(), destinations.len());
let feerate_vb: f32 = {
let fr: u16 = feerate_vb.try_into().map_err(|_| {
SpendCreationError::InsaneFees(InsaneFeeInfo::TooHighFeerate(feerate_vb))
})?;
fr
}
.into();
let max_sat_wu = main_descriptor
.max_sat_weight()
.try_into()
.expect("Weight must fit in a u32");
select_coins_for_spend(
candidate_coins,
tx.clone(),
change_txo.clone(),
feerate_vb,
min_fee,
max_sat_wu,
is_self_send,
)
.map_err(SpendCreationError::CoinSelection)?
};
let has_change = change_amount.to_sat() > 0;
if has_change {
check_output_value(change_amount)?;
let mut psbt_out = PsbtOut::default();
if let Some(AddrInfo { index, is_change }) = change_addr.info {
let desc = if is_change {
main_descriptor.change_descriptor()
} else {
main_descriptor.receive_descriptor()
};
desc.derive(index, secp)
.update_change_psbt_out(&mut psbt_out);
}
change_txo.value = change_amount;
tx.output.push(change_txo);
psbt_outs.push(psbt_out);
} else if max_change_amount.to_sat() > 0 {
warnings.push(CreateSpendWarning::ChangeAddedToFee(
max_change_amount.to_sat(),
));
}
if fee_for_ancestors.to_sat() > 0 {
warnings.push(CreateSpendWarning::AdditionalFeeForAncestors(
fee_for_ancestors.to_sat(),
));
}
let mut psbt_ins = Vec::with_capacity(selected.len());
for cand in &selected {
let sequence = cand
.sequence
.unwrap_or(bitcoin::Sequence::ENABLE_RBF_NO_LOCKTIME);
tx.input.push(bitcoin::TxIn {
previous_output: cand.outpoint,
sequence,
..bitcoin::TxIn::default()
});
let mut psbt_in = PsbtIn::default();
let coin_desc = derived_desc(secp, main_descriptor, cand);
coin_desc.update_psbt_in(&mut psbt_in);
psbt_in.witness_utxo = Some(bitcoin::TxOut {
value: cand.amount,
script_pubkey: coin_desc.script_pubkey(),
});
if !main_descriptor.is_taproot() {
psbt_in.non_witness_utxo = tx_getter.get_tx(&cand.outpoint.txid);
}
psbt_ins.push(psbt_in);
}
let psbt = Psbt {
unsigned_tx: tx,
version: 0,
xpub: BTreeMap::new(),
proprietary: BTreeMap::new(),
unknown: BTreeMap::new(),
inputs: psbt_ins,
outputs: psbt_outs,
};
sanity_check_psbt(main_descriptor, &psbt)?;
Ok(CreateSpendRes {
psbt,
has_change,
warnings,
})
}