diff --git a/src/commands/mod.rs b/src/commands/mod.rs index bf473bf6..06ccae39 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -9,7 +9,7 @@ use crate::{ database::{Coin, DatabaseConnection, DatabaseInterface}, descriptors, spend::{ - create_spend, AddrInfo, CandidateCoin, CreateSpendRes, SpendCreationError, + create_spend, AddrInfo, AncestorInfo, CandidateCoin, CreateSpendRes, SpendCreationError, SpendOutputAddress, SpendTxFees, TxGetter, }, DaemonControl, VERSION, @@ -179,6 +179,7 @@ fn coin_to_candidate( coin: &Coin, must_select: bool, sequence: Option, + ancestor_info: Option, ) -> CandidateCoin { CandidateCoin { outpoint: coin.outpoint, @@ -187,6 +188,7 @@ fn coin_to_candidate( is_change: coin.is_change, must_select, sequence, + ancestor_info, } } @@ -466,7 +468,10 @@ impl DaemonControl { .coins(&[CoinStatus::Confirmed], &[]) .into_values() .map(|c| { - coin_to_candidate(&c, /*must_select=*/ false, /*sequence=*/ None) + coin_to_candidate( + &c, /*must_select=*/ false, /*sequence=*/ None, + /*ancestor_info=*/ None, + ) }) .collect() } else { @@ -483,7 +488,12 @@ impl DaemonControl { } coins .into_values() - .map(|c| coin_to_candidate(&c, /*must_select=*/ true, /*sequence=*/ None)) + .map(|c| { + coin_to_candidate( + &c, /*must_select=*/ true, /*sequence=*/ None, + /*ancestor_info=*/ None, + ) + }) .collect() }; @@ -795,7 +805,10 @@ impl DaemonControl { let mut candidate_coins: Vec = prev_coins .values() .map(|c| { - coin_to_candidate(c, /*must_select=*/ !is_cancel, /*sequence=*/ None) + coin_to_candidate( + c, /*must_select=*/ !is_cancel, /*sequence=*/ None, + /*ancestor_info=*/ None, + ) }) .collect(); let confirmed_cands: Vec = db_conn @@ -807,6 +820,7 @@ impl DaemonControl { if !prev_coins.contains_key(&c.outpoint) { Some(coin_to_candidate( &c, /*must_select=*/ false, /*sequence=*/ None, + /*ancestor_info=*/ None, )) } else { None @@ -996,6 +1010,7 @@ impl DaemonControl { &c, /*must_select=*/ true, /*sequence=*/ Some(bitcoin::Sequence::from_height(timelock)), + /*ancestor_info=*/ None, )) } else { None diff --git a/src/spend.rs b/src/spend.rs index 4b9435d7..4f2ee7b6 100644 --- a/src/spend.rs +++ b/src/spend.rs @@ -1,4 +1,4 @@ -use crate::descriptors; +use crate::{bitcoin::MempoolEntry, descriptors}; use std::{collections::BTreeMap, convert::TryInto, fmt}; @@ -11,6 +11,7 @@ use miniscript::bitcoin::{ self, absolute::{Height, LockTime}, bip32, + constants::WITNESS_SCALE_FACTOR, psbt::{Input as PsbtIn, Output as PsbtOut, Psbt}, secp256k1, }; @@ -154,6 +155,29 @@ fn sanity_check_psbt( Ok(()) } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct AncestorInfo { + pub vsize: u32, + pub fee: u32, +} + +impl From 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"), + } + } +} + /// A candidate for coin selection when creating a transaction. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct CandidateCoin { @@ -169,6 +193,8 @@ pub struct CandidateCoin { pub must_select: bool, /// The nSequence field to set for an input spending this coin. pub sequence: Option, + /// Information about in-mempool ancestors of the coin. + pub ancestor_info: Option, } /// A coin selection result. @@ -291,12 +317,43 @@ fn select_coins_for_spend( base_weight = base_weight.saturating_sub(2); } let max_input_weight = TXIN_BASE_WEIGHT + max_sat_weight; + // Get feerate as u32 for calculation relating to ancestor below. + // We expect `feerate_vb` to be a positive integer, but take ceil() + // just in case to be sure we pay enough for ancestors. + 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 candidates: Vec = candidate_coins .iter() .map(|cand| Candidate { input_count: 1, value: cand.amount.to_sat(), - weight: max_input_weight, + weight: { + let extra = cand + .ancestor_info + .map(|info| { + // The implied ancestor vsize if the fee had been paid at our target feerate. + let ancestor_vsize_at_feerate: u32 = info + .fee + .checked_div(feerate_vb_u32) + .expect("feerate is greater than zero"); + // If the actual ancestor vsize is bigger than the implied vsize, we will need to + // pay the difference in order for the combined feerate to be at the target value. + // We multiply the vsize by 4 to get the ancestor weight, which is an upper bound + // on its true weight (vsize*4 - 3 <= weight <= vsize*4), to ensure we pay enough. + // Note that if candidates share ancestors, we may add this difference more than + // once in the resulting transaction. + info.vsize + .saturating_sub(ancestor_vsize_at_feerate) + .checked_mul(witness_factor) + .expect("weight difference must fit in u32") + }) + .unwrap_or(0); + max_input_weight + .checked_add(extra) + .expect("effective weight must fit in u32") + }, is_segwit: true, // We only support receiving on Segwit scripts. }) .collect();