Skip to content

Commit

Permalink
bump bdk_coin_select to 0.4
Browse files Browse the repository at this point in the history
Weights are now stored as `u64` instead of `u32`.

The tests have been updated to reflect that coin selection now
calculates feerates using a transaction's size in rounded-up
vbytes.
  • Loading branch information
jp1ac4 committed Jan 30, 2025
1 parent b98cff0 commit 9cb17a7
Show file tree
Hide file tree
Showing 6 changed files with 33 additions and 71 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion liana/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ description = "Liana development kit"
miniscript = { version = "12.0", features = ["serde", "compiler", "base64"] }

# Coin selection algorithms for spend transaction creation.
bdk_coin_select = "0.3"
bdk_coin_select = "0.4"
# We use TOML for the config, and JSON for RPC
serde = { version = "1.0", features = ["derive"] }

Expand Down
47 changes: 19 additions & 28 deletions liana/src/spend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ fn sanity_check_psbt(

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AncestorInfo {
pub vsize: u32,
pub vsize: u64,
pub fee: u32,
}

Expand Down Expand Up @@ -269,35 +269,31 @@ fn select_coins_for_spend(
change_txo: bitcoin::TxOut,
feerate_vb: f32,
replaced_fee: Option<u64>,
max_sat_weight: u32,
max_sat_weight: u64,
must_have_change: bool,
) -> Result<CoinSelectionRes, InsufficientFunds> {
let out_value_nochange = base_tx.output.iter().map(|o| o.value.to_sat()).sum();
let out_weight_nochange: u32 = {
let mut total: u32 = 0;
let out_weight_nochange = {
let mut total: u64 = 0;
for output in &base_tx.output {
let weight: u32 = output
.weight()
.to_wu()
.try_into()
.expect("an output's weight must fit in u32");
let weight = output.weight().to_wu();
total = total
.checked_add(weight)
.expect("sum of transaction outputs' weights must fit in u32");
.expect("sum of transaction outputs' weights must fit in u64");
}
total
};
let n_outputs_nochange = base_tx.output.len();
let max_input_weight = TXIN_BASE_WEIGHT + max_sat_weight;
// Get feerate as u32 for calculation relating to ancestor below.
// Get feerate as u64 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
let feerate_vb_u64 = feerate_vb.ceil() as u64;
let witness_factor: u64 = WITNESS_SCALE_FACTOR
.try_into()
.expect("scale factor must fit in u32");
.expect("scale factor must fit in u64");
// 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 mut added_weights = HashMap::<bitcoin::OutPoint, u64>::with_capacity(candidate_coins.len());
let candidates: Vec<Candidate> = candidate_coins
.iter()
.map(|cand| Candidate {
Expand All @@ -308,9 +304,8 @@ fn select_coins_for_spend(
.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)
let ancestor_vsize_at_feerate = <u32 as Into<u64>>::into(info.fee)
.checked_div(feerate_vb_u64)
.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.
Expand All @@ -321,15 +316,15 @@ fn select_coins_for_spend(
info.vsize
.saturating_sub(ancestor_vsize_at_feerate)
.checked_mul(witness_factor)
.expect("weight difference must fit in u32")
.expect("weight difference must fit in u64")
})
.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")
.expect("effective weight must fit in u64")
},
is_segwit: true, // We only support receiving on Segwit scripts.
})
Expand All @@ -348,11 +343,7 @@ fn select_coins_for_spend(
// a potential difference in the size of the outputs count varint.
let feerate = FeeRate::from_sat_per_vb(feerate_vb);
let long_term_feerate = FeeRate::from_sat_per_vb(LONG_TERM_FEERATE_VB);
let change_output_weight: u32 = change_txo
.weight()
.to_wu()
.try_into()
.expect("output weight must fit in u32");
let change_output_weight = change_txo.weight().to_wu();
let drain_weights = DrainWeights {
output_weight: change_output_weight,
spend_weight: max_input_weight,
Expand Down Expand Up @@ -446,7 +437,7 @@ fn select_coins_for_spend(
.try_into()
.expect("value is non-negative"),
);
let mut total_added_weight: u32 = 0;
let mut total_added_weight: u64 = 0;
let selected = selector
.selected_indices()
.iter()
Expand All @@ -458,7 +449,7 @@ fn select_coins_for_spend(
.get(&cand.outpoint)
.expect("contains added weight for all candidates"),
)
.expect("should fit in u32")
.expect("should fit in u64")
})
.collect();
// Calculate added fee based on the feerate in sats/wu, which is the feerate used for coin selection.
Expand Down Expand Up @@ -719,7 +710,7 @@ pub fn create_spend(
let max_sat_wu = main_descriptor
.max_sat_weight(use_primary_path)
.try_into()
.expect("Weight must fit in a u32");
.expect("Weight must fit in a u64");
select_coins_for_spend(
candidate_coins,
tx.clone(),
Expand Down
10 changes: 2 additions & 8 deletions lianad/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -512,10 +512,7 @@ impl DaemonControl {
self.bitcoin
.mempool_entry(&op.txid)
.map(|info| AncestorInfo {
vsize: info
.ancestor_vsize
.try_into()
.expect("vsize must fit in u32"),
vsize: info.ancestor_vsize,
fee: info
.fees
.ancestor
Expand Down Expand Up @@ -559,10 +556,7 @@ impl DaemonControl {
self.bitcoin
.mempool_entry(&op.txid)
.map(|info| AncestorInfo {
vsize: info
.ancestor_vsize
.try_into()
.expect("vsize must fit in u32"),
vsize: info.ancestor_vsize,
fee: info
.fees
.ancestor
Expand Down
2 changes: 1 addition & 1 deletion tests/test_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1187,7 +1187,7 @@ def test_rbfpsbt_bump_fee(lianad, bitcoind):
rbf_1_feerate = (
mempool_rbf_1["fees"]["ancestor"] * COIN / mempool_rbf_1["ancestorsize"]
)
assert 9.75 < rbf_1_feerate < 10.25
assert 10 <= rbf_1_feerate < 10.25
# If we try to RBF the first transaction again, it will not be possible as we
# deleted the PSBT above and the tx is no longer part of our wallet's
# spending txs (even though it's saved in the DB).
Expand Down
39 changes: 8 additions & 31 deletions tests/test_spend.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,17 +254,11 @@ def test_send_to_self(lianad, bitcoind):
lianad.rpc.broadcastspend(spend_txid)

# The only output is the change output so the feerate of the transaction must
# not be much lower than the one provided (it could be slightly lower since
# the change amount is determined using feerate in terms of sat/wu which, due
# to rounding, can lead to a slightly lower feerate in terms of sat/vb,
# especially when the number of inputs increases), and only possibly slightly
# higher (since we slightly overestimate the satisfaction size).
# not be lower than the one provided and only possibly slightly higher
# (since we slightly overestimate the satisfaction size).
res = bitcoind.rpc.getmempoolentry(spend_txid)
spend_feerate = res["fees"]["base"] * COIN / res["vsize"] # keep as decimal
if USE_TAPROOT:
assert specified_feerate <= spend_feerate < specified_feerate + 0.5
else:
assert specified_feerate - 0.5 < spend_feerate < specified_feerate + 0.5
assert specified_feerate <= spend_feerate < specified_feerate + 0.51

# We should by now only have one coin.
bitcoind.generate_block(1, wait_for_mempool=spend_txid)
Expand Down Expand Up @@ -334,7 +328,7 @@ def test_coin_selection(lianad, bitcoind):
)
# txid_1's feerate is approx 2 sat/vb as required.
txid_1_feerate = anc_fees / anc_vsize
assert 1.99 < txid_1_feerate < 2.01
assert 2 <= txid_1_feerate < 2.01
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 2)
# Check that change output is unconfirmed.
assert len(lianad.rpc.listcoins(["unconfirmed"])["coins"]) == 1
Expand Down Expand Up @@ -391,21 +385,7 @@ def test_coin_selection(lianad, bitcoind):
assert "psbt" in spend_res_2
spend_psbt_2 = PSBT.from_base64(spend_res_2["psbt"])
spend_txid_2 = spend_psbt_2.tx.txid().hex()
if USE_TAPROOT:
assert len(spend_res_2["warnings"]) == 0
else:
# We still get a warning in the non-taproot case.
assert len(spend_res_2["warnings"]) == 1
additional_fee_at_2satvb = additional_fees(anc_vsize, anc_fees, feerate)
assert additional_fee_at_3satvb > additional_fee_at_2satvb
assert len(spend_res_2["warnings"]) == 1
assert (
spend_res_2["warnings"][0]
== "CPFP: an unconfirmed input was selected. The current transaction fee "
f"was increased by {additional_fee_at_2satvb} sats to make the average "
"feerate of both the input and current transaction equal to the selected "
"feerate."
)
assert len(spend_res_2["warnings"]) == 0

# The spend is using the unconfirmed change.
assert spend_psbt_2.tx.vin[0].prevout.hash == uint256_from_str(
Expand Down Expand Up @@ -478,8 +458,8 @@ def test_coin_selection(lianad, bitcoind):
# If we subtract the extra that pays for the ancestor, the feerate is approximately
# at the target value.
assert (
feerate - 0.5
< ((mempool_txid_3["fees"]["base"] * COIN) - additional_fee)
feerate
<= ((mempool_txid_3["fees"]["base"] * COIN) - additional_fee)
/ mempool_txid_3["vsize"]
< feerate + 0.5
)
Expand Down Expand Up @@ -634,7 +614,4 @@ def test_tr_multisig_2_of_2_feerate_is_met(feerate, lianad_multisig_2_of_2, bitc
spend_fee = res["fees"]["base"] * COIN
spend_weight = res["weight"]
assert spend_weight == 646

# Note that due to https://github.com/wizardsardine/liana/issues/1132
# we do not round up vbytes before multiplying by feerate.
assert spend_fee == math.ceil((646.0 / 4.0) * feerate)
assert spend_fee == math.ceil(646.0 / 4.0) * feerate

0 comments on commit 9cb17a7

Please sign in to comment.