Skip to content

Commit c0d4320

Browse files
committed
spend: add warning about fee for ancestor
1 parent a38c173 commit c0d4320

File tree

2 files changed

+130
-19
lines changed

2 files changed

+130
-19
lines changed

src/spend.rs

+14-1
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,7 @@ pub enum SpendTxFees {
543543
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
544544
pub enum CreateSpendWarning {
545545
ChangeAddedToFee(u64),
546+
AddtionalFeeForAncestors(u64),
546547
}
547548

548549
impl fmt::Display for CreateSpendWarning {
@@ -554,6 +555,12 @@ impl fmt::Display for CreateSpendWarning {
554555
amt,
555556
if *amt > 1 {"s"} else {""},
556557
),
558+
CreateSpendWarning::AddtionalFeeForAncestors(amt) => write!(
559+
f,
560+
"An additional fee of {} sat{} has been added to pay for ancestors at the target feerate.",
561+
amt,
562+
if *amt > 1 {"s"} else {""},
563+
),
557564
}
558565
}
559566
}
@@ -673,7 +680,7 @@ pub fn create_spend(
673680
selected,
674681
change_amount,
675682
max_change_amount,
676-
..
683+
fee_for_ancestors,
677684
} = {
678685
// At this point the transaction still has no input and no change output, as expected
679686
// by the coins selection helper function.
@@ -735,6 +742,12 @@ pub fn create_spend(
735742
));
736743
}
737744

745+
if fee_for_ancestors.to_sat() > 0 {
746+
warnings.push(CreateSpendWarning::AddtionalFeeForAncestors(
747+
fee_for_ancestors.to_sat(),
748+
));
749+
}
750+
738751
// Iterate through selected coins and add necessary information to the PSBT inputs.
739752
let mut psbt_ins = Vec::with_capacity(selected.len());
740753
for cand in &selected {

tests/test_spend.py

+116-18
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,16 @@ def sign_and_broadcast(psbt):
171171
psbt = PSBT.from_base64(res["psbt"])
172172
sign_and_broadcast(psbt)
173173
assert len(psbt.o) == 4
174-
assert len(res["warnings"]) == 0
174+
assert bitcoind.rpc.getmempoolentry(deposit_d)["ancestorsize"] == 165
175+
assert bitcoind.rpc.getmempoolentry(deposit_d)["fees"]["ancestor"] * COIN == 165
176+
# ancestor vsize at feerate 2 sat/vb = ancestor_fee / 2 = 165 / 2 = 82
177+
# extra_weight <= (extra vsize * witness factor) = (165 - 82) * 4 = 332
178+
# additional fee at 2 sat/vb (0.5 sat/wu) = 332 * 0.5 = 166
179+
assert len(res["warnings"]) == 1
180+
assert (
181+
res["warnings"][0]
182+
== "An additional fee of 166 sats has been added to pay for ancestors at the target feerate."
183+
)
175184

176185
# All the spent coins must have been detected as such
177186
all_deposits = (deposit_a, deposit_b, deposit_c, deposit_d)
@@ -263,6 +272,7 @@ def test_coin_selection(lianad, bitcoind):
263272
# Coin selection now succeeds.
264273
spend_res_1 = lianad.rpc.createspend({dest_addr_1: 100_000}, [], 2)
265274
assert "psbt" in spend_res_1
275+
assert len(spend_res_1["warnings"]) == 0
266276
# Increase spend amount and we have insufficient funds again even though we
267277
# now have confirmed coins.
268278
assert "missing" in lianad.rpc.createspend({dest_addr_1: 200_000}, [], 2)
@@ -280,17 +290,61 @@ def test_coin_selection(lianad, bitcoind):
280290
assert lianad.rpc.listcoins(["unconfirmed"])["coins"][0]["is_change"] is True
281291
assert len(lianad.rpc.listcoins(["spending"])["coins"]) == 1
282292
# We can use unconfirmed change as candidate.
293+
# Depending on the feerate, we'll get a warning about paying extra for the ancestor.
283294
dest_addr_2 = bitcoind.rpc.getnewaddress()
295+
# If feerate is higher than ancestor, we'll need to pay extra.
296+
297+
# Try 10 sat/vb:
298+
spend_res_2 = lianad.rpc.createspend({dest_addr_2: 10_000}, [], 10)
299+
assert "psbt" in spend_res_2
300+
spend_psbt_2 = PSBT.from_base64(spend_res_2["psbt"])
301+
# The spend is using the unconfirmed change.
302+
assert spend_psbt_2.tx.vin[0].prevout.hash == uint256_from_str(
303+
bytes.fromhex(spend_txid_1)[::-1]
304+
)
305+
assert bitcoind.rpc.getmempoolentry(spend_txid_1)["ancestorsize"] == 161
306+
assert bitcoind.rpc.getmempoolentry(spend_txid_1)["fees"]["ancestor"] * COIN == 339
307+
# ancestor vsize at feerate 10 sat/vb = ancestor_fee / 10 = 339 / 10 = 33
308+
# extra_weight <= (extra vsize * witness factor) = (161 - 33) * 4 = 512
309+
# additional fee at 10 sat/vb (2.5 sat/wu) = 512 * 2.5 = 1280
310+
assert len(spend_res_2["warnings"]) == 1
311+
assert (
312+
spend_res_2["warnings"][0]
313+
== "An additional fee of 1280 sats has been added to pay for ancestors at the target feerate."
314+
)
315+
316+
# Try 3 sat/vb:
317+
spend_res_2 = lianad.rpc.createspend({dest_addr_2: 10_000}, [], 3)
318+
assert "psbt" in spend_res_2
319+
spend_psbt_2 = PSBT.from_base64(spend_res_2["psbt"])
320+
# The spend is using the unconfirmed change.
321+
assert spend_psbt_2.tx.vin[0].prevout.hash == uint256_from_str(
322+
bytes.fromhex(spend_txid_1)[::-1]
323+
)
324+
assert bitcoind.rpc.getmempoolentry(spend_txid_1)["ancestorsize"] == 161
325+
assert bitcoind.rpc.getmempoolentry(spend_txid_1)["fees"]["ancestor"] * COIN == 339
326+
# ancestor vsize at feerate 3 sat/vb = ancestor_fee / 3 = 339 / 3 = 113
327+
# extra_weight <= (extra vsize * witness factor) = (161 - 113) * 4 = 192
328+
# additional fee at 3 sat/vb (0.75 sat/wu) = 192 * 0.75 = 144
329+
assert len(spend_res_2["warnings"]) == 1
330+
assert (
331+
spend_res_2["warnings"][0]
332+
== "An additional fee of 144 sats has been added to pay for ancestors at the target feerate."
333+
)
334+
335+
# 2 sat/vb is same feerate as ancestor and we have no warnings:
284336
spend_res_2 = lianad.rpc.createspend({dest_addr_2: 10_000}, [], 2)
285337
assert "psbt" in spend_res_2
338+
assert len(spend_res_2["warnings"]) == 0
286339
spend_psbt_2 = PSBT.from_base64(spend_res_2["psbt"])
287340
# The spend is using the unconfirmed change.
288341
assert spend_psbt_2.tx.vin[0].prevout.hash == uint256_from_str(
289342
bytes.fromhex(spend_txid_1)[::-1]
290343
)
344+
291345
# Get another coin to check coin selection with more than one candidate.
292346
recv_addr_2 = lianad.rpc.getnewaddress()["address"]
293-
deposit_2 = bitcoind.rpc.sendtoaddress(recv_addr_2, 0.0002) # 20_000 sats
347+
deposit_2 = bitcoind.rpc.sendtoaddress(recv_addr_2, 30_000 / COIN)
294348
wait_for(lambda: len(lianad.rpc.listcoins(["unconfirmed"])["coins"]) == 2)
295349
assert (
296350
len(
@@ -302,33 +356,77 @@ def test_coin_selection(lianad, bitcoind):
302356
)
303357
== 1
304358
)
305-
# As only one unconfirmed coin is change, we have insufficient funds.
306359
dest_addr_3 = bitcoind.rpc.getnewaddress()
307-
assert "missing" in lianad.rpc.createspend({dest_addr_3: 30_000}, [], 2)
308-
# Now confirm both coins.
309-
bitcoind.generate_block(1, wait_for_mempool=deposit_2)
310-
wait_for(lambda: len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 2)
311-
spend_res_3 = lianad.rpc.createspend({dest_addr_3: 30_000}, [], 2)
360+
# As only one unconfirmed coin is change, we have insufficient funds.
361+
assert "missing" in lianad.rpc.createspend({dest_addr_3: 20_000}, [], 10)
362+
363+
# If we include both unconfirmed coins manually, it will succeed.
364+
# We'll need to pay extra for each unconfirmed coin's ancestors.
365+
outpoints = [c["outpoint"] for c in lianad.rpc.listcoins(["unconfirmed"])["coins"]]
366+
367+
spend_res_3 = lianad.rpc.createspend({dest_addr_3: 20_000}, outpoints, 10)
312368
assert "psbt" in spend_res_3
369+
assert bitcoind.rpc.getmempoolentry(deposit_2)["ancestorsize"] == 165
370+
assert bitcoind.rpc.getmempoolentry(deposit_2)["fees"]["ancestor"] * COIN == 165
371+
# From above, extra fee for unconfirmed change at 10 sat/vb = 1280.
372+
# For unconfirmed non-change:
373+
# ancestor vsize at feerate 10 sat/vb = ancestor_fee / 10 = 165 / 10 = 16
374+
# extra_weight <= (extra vsize * witness factor) = (165 - 16) * 4 = 596
375+
# additional fee at 10 sat/vb (2.5 sat/wu) = 596 * 2.5 = 1490
376+
# Sum of extra ancestor fees = 1280 + 1490 = 2770.
377+
assert len(spend_res_3["warnings"]) == 1
378+
assert (
379+
spend_res_3["warnings"][0]
380+
== "An additional fee of 2770 sats has been added to pay for ancestors at the target feerate."
381+
)
382+
spend_psbt_3 = PSBT.from_base64(spend_res_3["psbt"])
383+
spend_txid_3 = sign_and_broadcast_psbt(lianad, spend_psbt_3)
384+
mempool_txid_3 = bitcoind.rpc.getmempoolentry(spend_txid_3)
385+
# The effective feerate of new transaction plus ancestor matches the target.
386+
# Note that in the mempool entry, "ancestor" includes spend_txid_3 itself.
387+
assert (
388+
mempool_txid_3["fees"]["ancestor"] * COIN // mempool_txid_3["ancestorsize"]
389+
== 10
390+
)
391+
# The spend_txid_3 transaction itself has a higher feerate.
392+
assert (mempool_txid_3["fees"]["base"] * COIN) // mempool_txid_3["vsize"] > 10
393+
# If we subtract the extra that pays for the ancestor, the feerate is at the target value.
394+
assert ((mempool_txid_3["fees"]["base"] * COIN) - 2770) // mempool_txid_3[
395+
"vsize"
396+
] == 10
397+
398+
# Now confirm the spend.
399+
bitcoind.generate_block(1, wait_for_mempool=spend_txid_3)
400+
wait_for(lambda: len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 1)
401+
402+
# Now create the same spend with auto and manual selection:
403+
dest_addr_4 = bitcoind.rpc.getnewaddress()
404+
spend_res_4 = lianad.rpc.createspend({dest_addr_4: 15_000}, [], 2)
405+
assert "psbt" in spend_res_4
406+
assert len(spend_res_4["warnings"]) == 0
313407

314408
# The transaction contains a change output.
315-
spend_psbt_3 = PSBT.from_base64(spend_res_3["psbt"])
316-
assert len(spend_psbt_3.i) == 2
317-
assert len(spend_psbt_3.o) == 2
318-
assert len(spend_psbt_3.tx.vout) == 2
409+
spend_psbt_4 = PSBT.from_base64(spend_res_4["psbt"])
410+
assert len(spend_psbt_4.i) == 1
411+
assert len(spend_psbt_4.o) == 2
412+
assert len(spend_psbt_4.tx.vout) == 2
319413

320414
# Now create a transaction with manual coin selection using the same outpoints.
321415
outpoints = [
322-
f"{txin.prevout.hash:064x}:{txin.prevout.n}" for txin in spend_psbt_3.tx.vin
416+
f"{txin.prevout.hash:064x}:{txin.prevout.n}" for txin in spend_psbt_4.tx.vin
323417
]
324-
res_manual = lianad.rpc.createspend({dest_addr_3: 30_000}, outpoints, 2)
418+
assert len(outpoints) > 0
419+
res_manual = lianad.rpc.createspend({dest_addr_4: 15_000}, outpoints, 2)
420+
assert len(res_manual["warnings"]) == 0
325421
psbt_manual = PSBT.from_base64(res_manual["psbt"])
326422

327423
# Recipient details are the same for both.
328-
assert spend_psbt_3.tx.vout[0].nValue == psbt_manual.tx.vout[0].nValue
329-
assert spend_psbt_3.tx.vout[0].scriptPubKey == psbt_manual.tx.vout[0].scriptPubKey
330-
# Change amount is the same (change address will be different).
331-
assert spend_psbt_3.tx.vout[1].nValue == psbt_manual.tx.vout[1].nValue
424+
assert spend_psbt_4.tx.vout[0].nValue == psbt_manual.tx.vout[0].nValue
425+
assert spend_psbt_4.tx.vout[0].scriptPubKey == psbt_manual.tx.vout[0].scriptPubKey
426+
# Change details are also the same
427+
# (change address is same as neither transaction has been broadcast)
428+
assert spend_psbt_4.tx.vout[1].nValue == psbt_manual.tx.vout[1].nValue
429+
assert spend_psbt_4.tx.vout[1].scriptPubKey == psbt_manual.tx.vout[1].scriptPubKey
332430

333431

334432
def test_coin_selection_changeless(lianad, bitcoind):

0 commit comments

Comments
 (0)