Merge #873: commands: include unconfirmed change as coin selection candidates

c0d432057df6b2f0bc879f718489c761867fa88c spend: add warning about fee for ancestor (jp1ac4)
a38c1739b6d6bdc5944e2300f1cdf9121babf7d1 spend: return additional fee paid for ancestors (jp1ac4)
62bb4aded43a379bcf5607fd48c492fcfb84bfd5 commands: include unconfirmed change as candidates (jp1ac4)
b05b0f14e5e08ab32e78ea263531d8a587c5b184 commands: add ancestor info for user-selected unconfirmed coins (jp1ac4)
94ef66c03a17221cc9cddb8d1d463a46f0b489c9 spend: increase candidate weight to pay for ancestor (jp1ac4)
5f0022083df8354ceb03d79311317e378296629d bitcoin: add mempool_entry to interface (jp1ac4)
edbf00f17c3796dc326bfbe6ae8833c201bdc045 bitcoin: add ancestor size and fees to mempool entry (jp1ac4)
04503225bcd066dd7d4099997c56b836c29d5b31 func test: use utils function (jp1ac4)

Pull request description:

  This is a first PR towards resolving #826 that adds unconfirmed change as coin selection candidates when creating a spend.

  As per https://github.com/wizardsardine/liana/issues/826#issuecomment-1853058734, I haven't made any changes to the `rbfpsbt` command.

  We will also need to apply the same change in the GUI when selecting candidates for coin selection there (see https://github.com/wizardsardine/liana/pull/863#discussion_r1422823362).

ACKs for top commit:
  darosior:
    ACK c0d4320

Tree-SHA512: 8c17f5f8c32913f1ffae3a93ca3e8ee52ac40ee86790e41d73def5ed0c057e110e101797f778715fcd5f6bded1cd170618209323b5114a4f69c02d0ce066a2f2
This commit is contained in:
Antoine Poinsot 2024-03-09 18:48:49 +01:00
commit 1fe5acb673
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
7 changed files with 368 additions and 90 deletions

View File

@ -149,7 +149,8 @@ A coin may have one of the following four statuses:
Create a transaction spending one or more of our coins. All coins must exist and not be spent.
If no coins are specified in `outpoints`, they will be selected automatically from the set of
confirmed coins (see [`listcoins`](#listcoins) for coin status definitions).
confirmed coins together with any unconfirmed coins that are change outputs
(see [`listcoins`](#listcoins) for coin status definitions).
Will error if the given coins are not sufficient to cover the transaction cost at 90% (or more) of
the given feerate. If on the contrary the transaction is more than sufficiently funded, it will

View File

@ -1473,6 +1473,7 @@ impl<'a> CachedTxGetter<'a> {
#[derive(Debug, Clone)]
pub struct MempoolEntry {
pub vsize: u64,
pub ancestor_vsize: u64,
pub fees: MempoolEntryFees,
}
@ -1482,19 +1483,28 @@ impl From<Json> for MempoolEntry {
.get("vsize")
.and_then(Json::as_u64)
.expect("Must be present in bitcoind response");
let ancestor_vsize = json
.get("ancestorsize")
.and_then(Json::as_u64)
.expect("Must be present in bitcoind response");
let fees = json
.get("fees")
.as_ref()
.expect("Must be present in bitcoind response")
.into();
MempoolEntry { vsize, fees }
MempoolEntry {
vsize,
ancestor_vsize,
fees,
}
}
}
#[derive(Debug, Clone)]
pub struct MempoolEntryFees {
pub base: bitcoin::Amount,
pub ancestor: bitcoin::Amount,
pub descendant: bitcoin::Amount,
}
@ -1506,11 +1516,20 @@ impl From<&&Json> for MempoolEntryFees {
.and_then(Json::as_f64)
.and_then(|a| bitcoin::Amount::from_btc(a).ok())
.expect("Must be present and a valid amount");
let ancestor = json
.get("ancestor")
.and_then(Json::as_f64)
.and_then(|a| bitcoin::Amount::from_btc(a).ok())
.expect("Must be present and a valid amount");
let descendant = json
.get("descendant")
.and_then(Json::as_f64)
.and_then(|a| bitcoin::Amount::from_btc(a).ok())
.expect("Must be present and a valid amount");
MempoolEntryFees { base, descendant }
MempoolEntryFees {
base,
ancestor,
descendant,
}
}
}

View File

@ -116,6 +116,11 @@ pub trait BitcoinInterface: Send {
/// Get the details of unconfirmed transactions spending these outpoints, if any.
fn mempool_spenders(&self, outpoints: &[bitcoin::OutPoint]) -> Vec<MempoolEntry>;
/// Get mempool data for the given transaction.
///
/// Returns `None` if the transaction is not in the mempool.
fn mempool_entry(&self, txid: &bitcoin::Txid) -> Option<MempoolEntry>;
}
impl BitcoinInterface for d::BitcoinD {
@ -366,6 +371,10 @@ impl BitcoinInterface for d::BitcoinD {
.filter_map(|txid| self.mempool_entry(&txid))
.collect()
}
fn mempool_entry(&self, txid: &bitcoin::Txid) -> Option<MempoolEntry> {
self.mempool_entry(txid)
}
}
// FIXME: do we need to repeat the entire trait implemenation? Isn't there a nicer way?
@ -456,6 +465,10 @@ impl BitcoinInterface for sync::Arc<sync::Mutex<dyn BitcoinInterface + 'static>>
fn mempool_spenders(&self, outpoints: &[bitcoin::OutPoint]) -> Vec<MempoolEntry> {
self.lock().unwrap().mempool_spenders(outpoints)
}
fn mempool_entry(&self, txid: &bitcoin::Txid) -> Option<MempoolEntry> {
self.lock().unwrap().mempool_entry(txid)
}
}
// FIXME: We could avoid this type (and all the conversions entailing allocations) if bitcoind

View File

@ -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<bitcoin::Sequence>,
ancestor_info: Option<AncestorInfo>,
) -> CandidateCoin {
CandidateCoin {
outpoint: coin.outpoint,
@ -187,6 +188,7 @@ fn coin_to_candidate(
is_change: coin.is_change,
must_select,
sequence,
ancestor_info,
}
}
@ -460,13 +462,33 @@ impl DaemonControl {
// the spend from a set of optional candidates.
// Otherwise, only the specified coins will be used, all as mandatory candidates.
let candidate_coins: Vec<CandidateCoin> = if coins_outpoints.is_empty() {
// We only select confirmed coins for now. Including unconfirmed ones as well would
// introduce a whole bunch of additional complexity.
// From our unconfirmed coins, we only include those that are change outputs
// since unconfirmed external deposits are more at risk of being dropped
// unexpectedly from the mempool as they are beyond the user's control.
db_conn
.coins(&[CoinStatus::Confirmed], &[])
.into_values()
.map(|c| {
coin_to_candidate(&c, /*must_select=*/ false, /*sequence=*/ None)
.coins(&[CoinStatus::Unconfirmed, CoinStatus::Confirmed], &[])
.into_iter()
.filter_map(|(op, c)| {
if c.block_info.is_some() {
Some((c, None)) // confirmed coins have no ancestor info
} else if c.is_change {
// In case the mempool_entry is None, the coin will be included without
// any ancestor info.
Some((
c,
self.bitcoin.mempool_entry(&op.txid).map(AncestorInfo::from),
))
} else {
None
}
})
.map(|(c, ancestor_info)| {
coin_to_candidate(
&c,
/*must_select=*/ false,
/*sequence=*/ None,
ancestor_info,
)
})
.collect()
} else {
@ -482,8 +504,23 @@ impl DaemonControl {
}
}
coins
.into_values()
.map(|c| coin_to_candidate(&c, /*must_select=*/ true, /*sequence=*/ None))
.into_iter()
.map(|(op, c)| {
let ancestor_info = if c.block_info.is_none() {
// We include any non-change coins here as they have been selected by the caller.
// If the unconfirmed coin's transaction is no longer in the mempool, keep the
// coin as a candidate but without any ancestor info (same as confirmed candidate).
self.bitcoin.mempool_entry(&op.txid).map(AncestorInfo::from)
} else {
None
};
coin_to_candidate(
&c,
/*must_select=*/ true,
/*sequence=*/ None,
ancestor_info,
)
})
.collect()
};
@ -795,7 +832,14 @@ impl DaemonControl {
let mut candidate_coins: Vec<CandidateCoin> = prev_coins
.values()
.map(|c| {
coin_to_candidate(c, /*must_select=*/ !is_cancel, /*sequence=*/ None)
// In case any previous coins are unconfirmed, we don't include their ancestor info
// in the candidate as the replacement fee and feerate will be higher and any
// additional fee to pay for ancestors should already have been taken into account
// when including these coins in the previous transaction.
coin_to_candidate(
c, /*must_select=*/ !is_cancel, /*sequence=*/ None,
/*ancestor_info=*/ None,
)
})
.collect();
let confirmed_cands: Vec<CandidateCoin> = db_conn
@ -807,6 +851,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 +1041,7 @@ impl DaemonControl {
&c,
/*must_select=*/ true,
/*sequence=*/ Some(bitcoin::Sequence::from_height(timelock)),
/*ancestor_info=*/ None,
))
} else {
None
@ -1384,8 +1430,8 @@ mod tests {
spend_txid: None,
spend_block: None,
}]);
// If we try to use coin selection, the unconfirmed coin will not be used as a candidate
// and so we get a coin selection error due to insufficient funds.
// If we try to use coin selection, the unconfirmed non-change coin will not be used
// as a candidate and so we get a coin selection error due to insufficient funds.
assert!(matches!(
control.create_spend(&destinations, &[], 1, None),
Ok(CreateSpendResult::InsufficientFunds { .. }),
@ -1616,7 +1662,7 @@ mod tests {
)))
);
// Add a confirmed unspent coin to be used for coin selection.
// Add an unconfirmed change coin to be used for coin selection.
let confirmed_op_1 = bitcoin::OutPoint {
txid: dummy_op.txid,
vout: dummy_op.vout + 100,
@ -1624,13 +1670,10 @@ mod tests {
db_conn.new_unspent_coins(&[Coin {
outpoint: confirmed_op_1,
is_immature: false,
block_info: Some(BlockInfo {
height: 174500,
time: 174500,
}),
block_info: None,
amount: bitcoin::Amount::from_sat(80_000),
derivation_index: bip32::ChildNumber::from(42),
is_change: false,
is_change: true,
spend_txid: None,
spend_block: None,
}]);

View File

@ -1,6 +1,10 @@
use crate::descriptors;
use crate::{bitcoin::MempoolEntry, descriptors};
use std::{collections::BTreeMap, convert::TryInto, fmt};
use std::{
collections::{BTreeMap, HashMap},
convert::TryInto,
fmt,
};
pub use bdk_coin_select::InsufficientFunds;
use bdk_coin_select::{
@ -11,6 +15,7 @@ use miniscript::bitcoin::{
self,
absolute::{Height, LockTime},
bip32,
constants::WITNESS_SCALE_FACTOR,
psbt::{Input as PsbtIn, Output as PsbtOut, Psbt},
secp256k1,
};
@ -154,6 +159,29 @@ fn sanity_check_psbt(
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"),
}
}
}
/// A candidate for coin selection when creating a transaction.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CandidateCoin {
@ -169,6 +197,8 @@ pub struct CandidateCoin {
pub must_select: bool,
/// The nSequence field to set for an input spending this coin.
pub sequence: Option<bitcoin::Sequence>,
/// Information about in-mempool ancestors of the coin.
pub ancestor_info: Option<AncestorInfo>,
}
/// A coin selection result.
@ -184,6 +214,8 @@ pub struct CoinSelectionRes {
/// Maximum change amount possible with the selection irrespective of any change
/// policy.
pub max_change_amount: bitcoin::Amount,
/// Fee added to pay for ancestors at the target feerate.
pub fee_for_ancestors: bitcoin::Amount,
}
/// Metric based on [`LowestFee`] that aims to minimize transaction fees
@ -291,12 +323,48 @@ 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");
// This will be used to store any extra weight added to candidates.
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: 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);
// Store the extra weight for this candidate for use later on.
// At the same time, make sure there are no duplicate outpoints.
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, // We only support receiving on Segwit scripts.
})
.collect();
@ -336,9 +404,10 @@ fn select_coins_for_spend(
// Finally, run the coin selection algorithm. We use an opportunistic BnB and if it couldn't
// find any solution we fall back to selecting coins by descending value.
let feerate = FeeRate::from_sat_per_vb(feerate_vb);
let target = Target {
value: out_value_nochange,
feerate: FeeRate::from_sat_per_vb(feerate_vb),
feerate,
min_fee,
};
let lowest_fee = LowestFee {
@ -402,14 +471,29 @@ fn select_coins_for_spend(
.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();
// Calculate added fee based on the feerate in sats/wu, which is the feerate used for coin selection.
let fee_for_ancestors =
bitcoin::Amount::from_sat(((total_added_weight as f32) * feerate.spwu()).ceil() as u64);
Ok(CoinSelectionRes {
selected: selector
.selected_indices()
.iter()
.map(|i| candidate_coins[*i])
.collect(),
selected,
change_amount,
max_change_amount,
fee_for_ancestors,
})
}
@ -459,6 +543,7 @@ pub enum SpendTxFees {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum CreateSpendWarning {
ChangeAddedToFee(u64),
AddtionalFeeForAncestors(u64),
}
impl fmt::Display for CreateSpendWarning {
@ -470,6 +555,12 @@ impl fmt::Display for CreateSpendWarning {
amt,
if *amt > 1 {"s"} else {""},
),
CreateSpendWarning::AddtionalFeeForAncestors(amt) => write!(
f,
"An additional fee of {} sat{} has been added to pay for ancestors at the target feerate.",
amt,
if *amt > 1 {"s"} else {""},
),
}
}
}
@ -589,6 +680,7 @@ pub fn create_spend(
selected,
change_amount,
max_change_amount,
fee_for_ancestors,
} = {
// At this point the transaction still has no input and no change output, as expected
// by the coins selection helper function.
@ -650,6 +742,12 @@ pub fn create_spend(
));
}
if fee_for_ancestors.to_sat() > 0 {
warnings.push(CreateSpendWarning::AddtionalFeeForAncestors(
fee_for_ancestors.to_sat(),
));
}
// Iterate through selected coins and add necessary information to the PSBT inputs.
let mut psbt_ins = Vec::with_capacity(selected.len());
for cand in &selected {

View File

@ -123,6 +123,10 @@ impl BitcoinInterface for DummyBitcoind {
fn mempool_spenders(&self, _: &[bitcoin::OutPoint]) -> Vec<MempoolEntry> {
Vec::new()
}
fn mempool_entry(&self, _: &bitcoin::Txid) -> Option<MempoolEntry> {
None
}
}
struct DummyDbState {

View File

@ -1,6 +1,6 @@
from fixtures import *
from test_framework.serializations import PSBT, uint256_from_str
from test_framework.utils import wait_for, COIN, RpcError
from test_framework.utils import sign_and_broadcast_psbt, wait_for, COIN, RpcError
def test_spend_change(lianad, bitcoind):
@ -59,14 +59,6 @@ def test_spend_change(lianad, bitcoind):
bitcoind.generate_block(1, wait_for_mempool=spend_txid)
def sign_and_broadcast_psbt(lianad, psbt):
txid = psbt.tx.txid().hex()
psbt = lianad.signer.sign_psbt(psbt)
lianad.rpc.updatespend(psbt.to_base64())
lianad.rpc.broadcastspend(txid)
return txid
def test_coin_marked_spent(lianad, bitcoind):
"""Test a spent coin is marked as such under various conditions."""
# Receive a coin in a single transaction
@ -179,7 +171,16 @@ def test_coin_marked_spent(lianad, bitcoind):
psbt = PSBT.from_base64(res["psbt"])
sign_and_broadcast(psbt)
assert len(psbt.o) == 4
assert len(res["warnings"]) == 0
assert bitcoind.rpc.getmempoolentry(deposit_d)["ancestorsize"] == 165
assert bitcoind.rpc.getmempoolentry(deposit_d)["fees"]["ancestor"] * COIN == 165
# ancestor vsize at feerate 2 sat/vb = ancestor_fee / 2 = 165 / 2 = 82
# extra_weight <= (extra vsize * witness factor) = (165 - 82) * 4 = 332
# additional fee at 2 sat/vb (0.5 sat/wu) = 332 * 0.5 = 166
assert len(res["warnings"]) == 1
assert (
res["warnings"][0]
== "An additional fee of 166 sats has been added to pay for ancestors at the target feerate."
)
# All the spent coins must have been detected as such
all_deposits = (deposit_a, deposit_b, deposit_c, deposit_d)
@ -249,84 +250,183 @@ def test_send_to_self(lianad, bitcoind):
def test_coin_selection(lianad, bitcoind):
"""We can create a spend using coin selection."""
# Send to an (external) address.
dest_100_000 = {bitcoind.rpc.getnewaddress(): 100_000}
dest_addr_1 = bitcoind.rpc.getnewaddress()
# Coin selection is not possible if we have no coins.
assert len(lianad.rpc.listcoins()["coins"]) == 0
assert "missing" in lianad.rpc.createspend(dest_100_000, [], 2)
assert "missing" in lianad.rpc.createspend({dest_addr_1: 100_000}, [], 2)
# Receive a coin in an unconfirmed deposit transaction.
recv_addr = lianad.rpc.getnewaddress()["address"]
deposit = bitcoind.rpc.sendtoaddress(recv_addr, 0.0008) # 80_000 sats
recv_addr_1 = lianad.rpc.getnewaddress()["address"]
deposit_1 = bitcoind.rpc.sendtoaddress(recv_addr_1, 0.0012) # 120_000 sats
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 1)
# There are still no confirmed coins to use as candidates for selection.
# There are still no confirmed coins or unconfirmed change
# to use as candidates for selection.
assert len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 0
assert len(lianad.rpc.listcoins(["unconfirmed"])["coins"]) == 1
assert "missing" in lianad.rpc.createspend(dest_100_000, [], 2)
assert lianad.rpc.listcoins(["unconfirmed"])["coins"][0]["is_change"] is False
assert "missing" in lianad.rpc.createspend({dest_addr_1: 100_000}, [], 2)
# Confirm coin.
bitcoind.generate_block(1, wait_for_mempool=deposit)
bitcoind.generate_block(1, wait_for_mempool=deposit_1)
wait_for(lambda: len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 1)
# Coin selection now succeeds.
spend_res_1 = lianad.rpc.createspend({dest_addr_1: 100_000}, [], 2)
assert "psbt" in spend_res_1
assert len(spend_res_1["warnings"]) == 0
# Increase spend amount and we have insufficient funds again even though we
# now have confirmed coins.
assert "missing" in lianad.rpc.createspend({dest_addr_1: 200_000}, [], 2)
# Insufficient funds for coin selection.
assert "missing" in lianad.rpc.createspend(dest_100_000, [], 2)
# Reduce spend amount.
dest_30_000 = {bitcoind.rpc.getnewaddress(): 30_000}
res = lianad.rpc.createspend(dest_30_000, [], 2)
assert "psbt" in res
# The transaction must contain a change output.
spend_psbt = PSBT.from_base64(res["psbt"])
assert len(spend_psbt.o) == 2
assert len(spend_psbt.tx.vout) == 2
# The transaction contains a change output.
spend_psbt_1 = PSBT.from_base64(spend_res_1["psbt"])
assert len(spend_psbt_1.o) == 2
assert len(spend_psbt_1.tx.vout) == 2
# Sign and broadcast this Spend transaction.
signed_psbt = lianad.signer.sign_psbt(spend_psbt)
lianad.rpc.updatespend(signed_psbt.to_base64())
spend_txid = signed_psbt.tx.txid().hex()
lianad.rpc.broadcastspend(spend_txid)
spend_txid_1 = sign_and_broadcast_psbt(lianad, spend_psbt_1)
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 2)
coins = lianad.rpc.listcoins()["coins"]
# Check that change output is unconfirmed.
assert len(lianad.rpc.listcoins(["unconfirmed"])["coins"]) == 1
assert lianad.rpc.listcoins(["unconfirmed"])["coins"][0]["is_change"] is True
assert len(lianad.rpc.listcoins(["spending"])["coins"]) == 1
# Check we cannot use coins as candidates if they are spending/spent or unconfirmed.
assert "missing" in lianad.rpc.createspend(dest_30_000, [], 2)
# We can use unconfirmed change as candidate.
# Depending on the feerate, we'll get a warning about paying extra for the ancestor.
dest_addr_2 = bitcoind.rpc.getnewaddress()
# If feerate is higher than ancestor, we'll need to pay extra.
# Now confirm the Spend.
bitcoind.generate_block(1, wait_for_mempool=spend_txid)
wait_for(lambda: len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 1)
# But its value is not enough for this Spend.
dest_60_000 = {bitcoind.rpc.getnewaddress(): 60_000}
assert "missing" in lianad.rpc.createspend(dest_60_000, [], 2)
# Try 10 sat/vb:
spend_res_2 = lianad.rpc.createspend({dest_addr_2: 10_000}, [], 10)
assert "psbt" in spend_res_2
spend_psbt_2 = PSBT.from_base64(spend_res_2["psbt"])
# The spend is using the unconfirmed change.
assert spend_psbt_2.tx.vin[0].prevout.hash == uint256_from_str(
bytes.fromhex(spend_txid_1)[::-1]
)
assert bitcoind.rpc.getmempoolentry(spend_txid_1)["ancestorsize"] == 161
assert bitcoind.rpc.getmempoolentry(spend_txid_1)["fees"]["ancestor"] * COIN == 339
# ancestor vsize at feerate 10 sat/vb = ancestor_fee / 10 = 339 / 10 = 33
# extra_weight <= (extra vsize * witness factor) = (161 - 33) * 4 = 512
# additional fee at 10 sat/vb (2.5 sat/wu) = 512 * 2.5 = 1280
assert len(spend_res_2["warnings"]) == 1
assert (
spend_res_2["warnings"][0]
== "An additional fee of 1280 sats has been added to pay for ancestors at the target feerate."
)
# Try 3 sat/vb:
spend_res_2 = lianad.rpc.createspend({dest_addr_2: 10_000}, [], 3)
assert "psbt" in spend_res_2
spend_psbt_2 = PSBT.from_base64(spend_res_2["psbt"])
# The spend is using the unconfirmed change.
assert spend_psbt_2.tx.vin[0].prevout.hash == uint256_from_str(
bytes.fromhex(spend_txid_1)[::-1]
)
assert bitcoind.rpc.getmempoolentry(spend_txid_1)["ancestorsize"] == 161
assert bitcoind.rpc.getmempoolentry(spend_txid_1)["fees"]["ancestor"] * COIN == 339
# ancestor vsize at feerate 3 sat/vb = ancestor_fee / 3 = 339 / 3 = 113
# extra_weight <= (extra vsize * witness factor) = (161 - 113) * 4 = 192
# additional fee at 3 sat/vb (0.75 sat/wu) = 192 * 0.75 = 144
assert len(spend_res_2["warnings"]) == 1
assert (
spend_res_2["warnings"][0]
== "An additional fee of 144 sats has been added to pay for ancestors at the target feerate."
)
# 2 sat/vb is same feerate as ancestor and we have no warnings:
spend_res_2 = lianad.rpc.createspend({dest_addr_2: 10_000}, [], 2)
assert "psbt" in spend_res_2
assert len(spend_res_2["warnings"]) == 0
spend_psbt_2 = PSBT.from_base64(spend_res_2["psbt"])
# The spend is using the unconfirmed change.
assert spend_psbt_2.tx.vin[0].prevout.hash == uint256_from_str(
bytes.fromhex(spend_txid_1)[::-1]
)
# Get another coin to check coin selection with more than one candidate.
recv_addr = lianad.rpc.getnewaddress()["address"]
deposit = bitcoind.rpc.sendtoaddress(recv_addr, 0.0002) # 20_000 sats
bitcoind.generate_block(1, wait_for_mempool=deposit)
wait_for(lambda: len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 2)
recv_addr_2 = lianad.rpc.getnewaddress()["address"]
deposit_2 = bitcoind.rpc.sendtoaddress(recv_addr_2, 30_000 / COIN)
wait_for(lambda: len(lianad.rpc.listcoins(["unconfirmed"])["coins"]) == 2)
assert (
len(
[
c
for c in lianad.rpc.listcoins(["unconfirmed"])["coins"]
if c["is_change"]
]
)
== 1
)
dest_addr_3 = bitcoind.rpc.getnewaddress()
# As only one unconfirmed coin is change, we have insufficient funds.
assert "missing" in lianad.rpc.createspend({dest_addr_3: 20_000}, [], 10)
res = lianad.rpc.createspend(dest_60_000, [], 2)
assert "psbt" in res
# If we include both unconfirmed coins manually, it will succeed.
# We'll need to pay extra for each unconfirmed coin's ancestors.
outpoints = [c["outpoint"] for c in lianad.rpc.listcoins(["unconfirmed"])["coins"]]
# The transaction must contain a change output.
auto_psbt = PSBT.from_base64(res["psbt"])
assert len(auto_psbt.o) == 2
assert len(auto_psbt.tx.vout) == 2
spend_res_3 = lianad.rpc.createspend({dest_addr_3: 20_000}, outpoints, 10)
assert "psbt" in spend_res_3
assert bitcoind.rpc.getmempoolentry(deposit_2)["ancestorsize"] == 165
assert bitcoind.rpc.getmempoolentry(deposit_2)["fees"]["ancestor"] * COIN == 165
# From above, extra fee for unconfirmed change at 10 sat/vb = 1280.
# For unconfirmed non-change:
# ancestor vsize at feerate 10 sat/vb = ancestor_fee / 10 = 165 / 10 = 16
# extra_weight <= (extra vsize * witness factor) = (165 - 16) * 4 = 596
# additional fee at 10 sat/vb (2.5 sat/wu) = 596 * 2.5 = 1490
# Sum of extra ancestor fees = 1280 + 1490 = 2770.
assert len(spend_res_3["warnings"]) == 1
assert (
spend_res_3["warnings"][0]
== "An additional fee of 2770 sats has been added to pay for ancestors at the target feerate."
)
spend_psbt_3 = PSBT.from_base64(spend_res_3["psbt"])
spend_txid_3 = sign_and_broadcast_psbt(lianad, spend_psbt_3)
mempool_txid_3 = bitcoind.rpc.getmempoolentry(spend_txid_3)
# The effective feerate of new transaction plus ancestor matches the target.
# Note that in the mempool entry, "ancestor" includes spend_txid_3 itself.
assert (
mempool_txid_3["fees"]["ancestor"] * COIN // mempool_txid_3["ancestorsize"]
== 10
)
# The spend_txid_3 transaction itself has a higher feerate.
assert (mempool_txid_3["fees"]["base"] * COIN) // mempool_txid_3["vsize"] > 10
# If we subtract the extra that pays for the ancestor, the feerate is at the target value.
assert ((mempool_txid_3["fees"]["base"] * COIN) - 2770) // mempool_txid_3[
"vsize"
] == 10
# Now confirm the spend.
bitcoind.generate_block(1, wait_for_mempool=spend_txid_3)
wait_for(lambda: len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 1)
# Now create the same spend with auto and manual selection:
dest_addr_4 = bitcoind.rpc.getnewaddress()
spend_res_4 = lianad.rpc.createspend({dest_addr_4: 15_000}, [], 2)
assert "psbt" in spend_res_4
assert len(spend_res_4["warnings"]) == 0
# The transaction contains a change output.
spend_psbt_4 = PSBT.from_base64(spend_res_4["psbt"])
assert len(spend_psbt_4.i) == 1
assert len(spend_psbt_4.o) == 2
assert len(spend_psbt_4.tx.vout) == 2
# Now create a transaction with manual coin selection using the same outpoints.
outpoints = [
f"{txin.prevout.hash:064x}:{txin.prevout.n}" for txin in auto_psbt.tx.vin
f"{txin.prevout.hash:064x}:{txin.prevout.n}" for txin in spend_psbt_4.tx.vin
]
res_manual = lianad.rpc.createspend(dest_60_000, outpoints, 2)
manual_psbt = PSBT.from_base64(res_manual["psbt"])
assert len(outpoints) > 0
res_manual = lianad.rpc.createspend({dest_addr_4: 15_000}, outpoints, 2)
assert len(res_manual["warnings"]) == 0
psbt_manual = PSBT.from_base64(res_manual["psbt"])
# Recipient details are the same for both.
assert auto_psbt.tx.vout[0].nValue == manual_psbt.tx.vout[0].nValue
assert auto_psbt.tx.vout[0].scriptPubKey == manual_psbt.tx.vout[0].scriptPubKey
# Change amount is the same (change address will be different).
assert auto_psbt.tx.vout[1].nValue == manual_psbt.tx.vout[1].nValue
assert spend_psbt_4.tx.vout[0].nValue == psbt_manual.tx.vout[0].nValue
assert spend_psbt_4.tx.vout[0].scriptPubKey == psbt_manual.tx.vout[0].scriptPubKey
# Change details are also the same
# (change address is same as neither transaction has been broadcast)
assert spend_psbt_4.tx.vout[1].nValue == psbt_manual.tx.vout[1].nValue
assert spend_psbt_4.tx.vout[1].scriptPubKey == psbt_manual.tx.vout[1].scriptPubKey
def test_coin_selection_changeless(lianad, bitcoind):