Skip to content

Commit 2ee92f1

Browse files
committed
pytest: enhance tests to test anchor and htlc tx feerates match targets.
Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
1 parent 9af8c64 commit 2ee92f1

File tree

2 files changed

+150
-4
lines changed

2 files changed

+150
-4
lines changed

tests/test_closing.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3777,9 +3777,9 @@ def test_closing_anchorspend_htlc_tx_rbf(node_factory, bitcoind):
37773777
fundsats = int(Millisatoshi(only_one(l1.rpc.listfunds()['outputs'])['amount_msat']).to_satoshi())
37783778
psbt = l1.rpc.fundpsbt("all", "1000perkw", 1000)['psbt']
37793779
# Pay 5k sats in fees, send most to l2
3780-
psbt = l1.rpc.addpsbtoutput(fundsats - 20000 - 5000, psbt, destination=l2.rpc.newaddr()['bech32'])['psbt']
3781-
# 10x2000 sat outputs for l1 to use.
3782-
for i in range(10):
3780+
psbt = l1.rpc.addpsbtoutput(fundsats - 24000 - 5000, psbt, destination=l2.rpc.newaddr()['bech32'])['psbt']
3781+
# 12x2000 sat outputs for l1 to use.
3782+
for i in range(12):
37833783
psbt = l1.rpc.addpsbtoutput(2000, psbt)['psbt']
37843784
l1.rpc.sendpsbt(l1.rpc.signpsbt(psbt)['signed_psbt'])
37853785
bitcoind.generate_block(1, wait_for_mempool=1)
@@ -3811,6 +3811,13 @@ def test_closing_anchorspend_htlc_tx_rbf(node_factory, bitcoind):
38113811

38123812
wait_for(lambda: len(bitcoind.rpc.getrawmempool()) == 2)
38133813

3814+
# Expect package feerate of 2000
3815+
details = bitcoind.rpc.getrawmempool(True).values()
3816+
total_weight = sum([d['weight'] for d in details])
3817+
total_fees = sum([float(d['fees']['base']) * 100_000_000 for d in details])
3818+
total_feerate_perkw = total_fees / total_weight * 1000
3819+
assert 2000 - 1 < total_feerate_perkw < 2000 + 1
3820+
38143821
# But we don't mine it! And fees go up again!
38153822
l1.set_feerates((3000, 3000, 3000, 3000))
38163823
bitcoind.generate_block(1, needfeerate=5000)
@@ -3819,6 +3826,13 @@ def test_closing_anchorspend_htlc_tx_rbf(node_factory, bitcoind):
38193826
# We actually resubmit the commit tx, then the RBF:
38203827
l1.daemon.wait_for_logs(['sendrawtx exit 0'] * 2)
38213828

3829+
# Expect package feerate of 3000
3830+
details = bitcoind.rpc.getrawmempool(True).values()
3831+
total_weight = sum([d['weight'] for d in details])
3832+
total_fees = sum([float(d['fees']['base']) * 100_000_000 for d in details])
3833+
total_feerate_perkw = total_fees / total_weight * 1000
3834+
assert 3000 - 1 < total_feerate_perkw < 3000 + 1
3835+
38223836
# And now we'll get it in (there's some rounding, so feerate a bit lower!)
38233837
bitcoind.generate_block(1, needfeerate=2990)
38243838

@@ -3840,8 +3854,15 @@ def test_closing_anchorspend_htlc_tx_rbf(node_factory, bitcoind):
38403854
# It will enter the mempool
38413855
wait_for(lambda: txid in bitcoind.rpc.getrawmempool())
38423856

3857+
tx = bitcoind.rpc.getrawmempool(True)[txid]
3858+
feerate_perkw = float(tx['fees']['base']) * 100_000_000 / tx['weight'] * 1000
3859+
# It actually has no change output, so it exceeds the fee quite a bit.
3860+
assert len(bitcoind.rpc.decoderawtransaction(bitcoind.rpc.getrawtransaction(txid))['vout']) == 1
3861+
assert feerate_perkw > 5000
3862+
38433863
# And this will mine it!
3844-
bitcoind.generate_block(1, needfeerate=4990)
3864+
bitcoind.generate_block(1, needfeerate=5000)
3865+
assert bitcoind.rpc.getrawmempool() == []
38453866

38463867

38473868
@pytest.mark.parametrize("anchors", [False, True])
@@ -4019,6 +4040,12 @@ def test_peer_anchor_push(node_factory, bitcoind, executor, chainparams):
40194040
# As blocks pass, we will use anchor to boost l3's tx.
40204041
for block, feerate in zip(range(120, 124), (12000, 13000, 14000, 15000)):
40214042
l2.daemon.wait_for_log(fr"Worth fee [0-9]*sat for remote commit tx to get 100000000msat at block 125 \(\+{125 - block}\) at feerate {feerate}perkw")
4043+
# Check feerate for entire package (commitment tx + anchor) is ~ correct
4044+
details = bitcoind.rpc.getrawmempool(True).values()
4045+
total_weight = sum([d['weight'] for d in details])
4046+
total_fees = sum([float(d['fees']['base']) * 100_000_000 for d in details])
4047+
total_feerate_perkw = total_fees / total_weight * 1000
4048+
assert feerate - 1 < total_feerate_perkw < feerate + 1
40224049
bitcoind.generate_block(1, needfeerate=16000)
40234050
sync_blockheight(bitcoind, [l2])
40244051
assert len(bitcoind.rpc.getrawmempool()) == 2

tests/test_wallet.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,19 @@ def test_txprepare_multi(node_factory, bitcoind):
287287
l1.rpc.txdiscard(prep['txid'])
288288

289289

290+
def feerate_from_psbt(bitcoind, node, psbt):
291+
# signpsbt insists they are reserved!
292+
node.rpc.reserveinputs(psbt, exclusive=False)
293+
final = node.rpc.dev_finalizepsbt(node.rpc.signpsbt(psbt)['signed_psbt'])
294+
node.rpc.unreserveinputs(psbt)
295+
psbt = node.rpc.setpsbtversion(final['psbt'], 0)['psbt']
296+
# analyzepsbt gives a vsize, but not a weight!
297+
# e.g. 'estimated_vsize': 356, 'estimated_feerate': Decimal('0.00030042'), 'fee': Decimal('0.00010695')
298+
fee = int(bitcoind.rpc.analyzepsbt(psbt)['fee'] * 100_000_000)
299+
weight = bitcoind.rpc.decoderawtransaction(final['tx'])['weight']
300+
return fee / weight * 1000
301+
302+
290303
def test_txprepare(node_factory, bitcoind, chainparams):
291304
amount = 1000000
292305
l1 = node_factory.get_node(random_hsm=True)
@@ -299,13 +312,18 @@ def test_txprepare(node_factory, bitcoind, chainparams):
299312

300313
bitcoind.generate_block(1)
301314
wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 10)
315+
for est in l1.rpc.feerates('perkw')['perkw']['estimates']:
316+
if est['blockcount'] == 12:
317+
normal_feerate_perkw = est['feerate']
302318

303319
prep = l1.rpc.txprepare(outputs=[{addr: Millisatoshi(amount * 3 * 1000)}])
304320
decode = bitcoind.rpc.decoderawtransaction(prep['unsigned_tx'])
305321
assert decode['txid'] == prep['txid']
306322
# 4 inputs, 2 outputs (3 if we have a fee output).
307323
assert len(decode['vin']) == 4
308324
assert len(decode['vout']) == 2 if not chainparams['feeoutput'] else 3
325+
# Feerate should be ~ as we asked for
326+
assert normal_feerate_perkw - 2 < feerate_from_psbt(bitcoind, l1, prep['psbt']) < normal_feerate_perkw + 2
309327

310328
# One output will be correct.
311329
outnum = [i for i, o in enumerate(decode['vout']) if o['value'] == Decimal(amount * 3) / 10**8][0]
@@ -333,6 +351,8 @@ def test_txprepare(node_factory, bitcoind, chainparams):
333351
assert decode['vout'][0]['value'] > Decimal(amount * 6) / 10**8 - Decimal(0.0002)
334352
assert decode['vout'][0]['scriptPubKey']['type'] == 'witness_v0_keyhash'
335353
assert scriptpubkey_addr(decode['vout'][0]['scriptPubKey']) == addr
354+
# Feerate should be ~ as we asked for
355+
assert normal_feerate_perkw - 2 < feerate_from_psbt(bitcoind, l1, prep2['psbt']) < normal_feerate_perkw + 2
336356

337357
# If I cancel the first one, I can get those first 4 outputs.
338358
discard = l1.rpc.txdiscard(prep['txid'])
@@ -351,6 +371,8 @@ def test_txprepare(node_factory, bitcoind, chainparams):
351371
assert decode['vout'][0]['value'] > Decimal(amount * 4) / 10**8 - Decimal(0.0002)
352372
assert decode['vout'][0]['scriptPubKey']['type'] == 'witness_v0_keyhash'
353373
assert scriptpubkey_addr(decode['vout'][0]['scriptPubKey']) == addr
374+
# Feerate should be ~ as we asked for
375+
assert normal_feerate_perkw - 1 < feerate_from_psbt(bitcoind, l1, prep3['psbt']) < normal_feerate_perkw + 1
354376

355377
# Cannot discard twice.
356378
with pytest.raises(RpcError, match=r'not an unreleased txid'):
@@ -371,20 +393,28 @@ def test_txprepare(node_factory, bitcoind, chainparams):
371393
assert decode['vout'][0]['value'] > Decimal(amount * 10) / 10**8 - Decimal(0.0003)
372394
assert decode['vout'][0]['scriptPubKey']['type'] == 'witness_v0_keyhash'
373395
assert scriptpubkey_addr(decode['vout'][0]['scriptPubKey']) == addr
396+
# Feerate should be ~ as we asked for
397+
assert normal_feerate_perkw - 2 < feerate_from_psbt(bitcoind, l1, prep4['psbt']) < normal_feerate_perkw + 2
374398
l1.rpc.txdiscard(prep4['txid'])
375399

376400
# Try passing in a utxo set
377401
utxos = [utxo["txid"] + ":" + str(utxo["output"])
378402
for utxo in l1.rpc.listfunds()["outputs"]][:4]
379403
prep5 = l1.rpc.txprepare([{addr:
380404
Millisatoshi(amount * 3.5 * 1000)}], utxos=utxos)
405+
# Feerate should be ~ as we asked for
406+
assert normal_feerate_perkw - 2 < feerate_from_psbt(bitcoind, l1, prep3['psbt']) < normal_feerate_perkw + 2
381407

382408
# Try passing unconfirmed utxos
383409
unconfirmed_utxo = l1.rpc.withdraw(l1.rpc.newaddr()["bech32"], 10**5)
384410
uutxos = [unconfirmed_utxo["txid"] + ":0"]
385411
with pytest.raises(RpcError, match=r"Could not afford"):
386412
l1.rpc.txprepare([{addr: Millisatoshi(amount * 3.5 * 1000)}],
387413
utxos=uutxos)
414+
# Feerate should be ~ as we asked for
415+
unconfirmed_tx = bitcoind.rpc.getrawmempool(True)[unconfirmed_utxo["txid"]]
416+
feerate_perkw = int(unconfirmed_tx['fees']['base'] * 100_000_000) * 1000 / unconfirmed_tx['weight']
417+
assert normal_feerate_perkw - 1 < feerate_perkw < normal_feerate_perkw + 1
388418

389419
decode = bitcoind.rpc.decoderawtransaction(prep5['unsigned_tx'])
390420
assert decode['txid'] == prep5['txid']
@@ -413,12 +443,16 @@ def test_txprepare(node_factory, bitcoind, chainparams):
413443
# You can have one which is all, but not two.
414444
prep5 = l1.rpc.txprepare([{addr: Millisatoshi(amount * 3 * 1000)},
415445
{addr: 'all'}])
446+
# Feerate should be ~ as we asked for
447+
assert normal_feerate_perkw - 2 < feerate_from_psbt(bitcoind, l1, prep5['psbt']) < normal_feerate_perkw + 2
416448
l1.rpc.txdiscard(prep5['txid'])
417449
with pytest.raises(RpcError, match=r"'all'"):
418450
prep5 = l1.rpc.txprepare([{addr: 'all'}, {addr: 'all'}])
419451

420452
prep5 = l1.rpc.txprepare([{addr: Millisatoshi(amount * 3 * 500 + 100000)},
421453
{addr: Millisatoshi(amount * 3 * 500 - 100000)}])
454+
# Feerate should be ~ as we asked for
455+
assert normal_feerate_perkw - 2 < feerate_from_psbt(bitcoind, l1, prep5['psbt']) < normal_feerate_perkw + 2
422456
decode = bitcoind.rpc.decoderawtransaction(prep5['unsigned_tx'])
423457
assert decode['txid'] == prep5['txid']
424458
# 4 inputs, 3 outputs(include change).
@@ -445,6 +479,91 @@ def test_txprepare(node_factory, bitcoind, chainparams):
445479
else:
446480
assert decode['vout'][changenum]['scriptPubKey']['type'] == 'witness_v1_taproot'
447481

482+
l1.rpc.txdiscard(prep5['txid'])
483+
484+
485+
def test_txprepare_feerate(node_factory, bitcoind, chainparams):
486+
# Make sure it works at different feerates!
487+
l1, l2 = node_factory.get_nodes(2)
488+
489+
# Add some funds to withdraw later
490+
for i in range(20):
491+
bitcoind.rpc.sendtoaddress(l1.rpc.newaddr()['bech32'],
492+
1000 / 10**8)
493+
494+
bitcoind.generate_block(1)
495+
out_addrs = l2.rpc.newaddr('all')
496+
wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 20)
497+
498+
for addrtype in ('bech32', 'p2tr'):
499+
for feerate in range(255, 1000, 250):
500+
prep = l1.rpc.txprepare([{out_addrs[addrtype]: Millisatoshi(9000)}], f"{feerate}perkw")
501+
actual_feerate = feerate_from_psbt(bitcoind, l1, prep['psbt'])
502+
assert feerate - 2 < actual_feerate
503+
# Feerate can be larger, if it chose not to give change output.
504+
if chainparams['elements']:
505+
fee_output = 1
506+
else:
507+
fee_output = 0
508+
if len(bitcoind.rpc.decoderawtransaction(prep['unsigned_tx'])['vout']) == 1 + 1 + fee_output:
509+
assert actual_feerate < feerate + 2
510+
l1.rpc.txdiscard(prep['txid'])
511+
512+
513+
@pytest.mark.parametrize("addrtype", ["bech32", "p2tr"])
514+
def test_fundpsbt_feerates(node_factory, bitcoind, chainparams, addrtype):
515+
if chainparams['elements'] and addrtype == 'p2tr':
516+
pytest.skip('No p2tr for elements')
517+
518+
l1 = node_factory.get_node()
519+
520+
# Add some funds to withdraw later
521+
for i in range(20):
522+
bitcoind.rpc.sendtoaddress(l1.rpc.newaddr(addrtype)[addrtype],
523+
5000 / 10**8)
524+
525+
# See utxo_spend_weight()
526+
if addrtype == 'bech32':
527+
witness_weight = 1 + 71 + 1 + 33
528+
elif addrtype == 'p2tr':
529+
witness_weight = 1 + 64
530+
else:
531+
assert False
532+
533+
input_weight = 1 + witness_weight + (32 + 4 + 4 + 1) * 4
534+
if chainparams['elements']:
535+
input_weight += 6
536+
537+
bitcoind.generate_block(1)
538+
wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 20)
539+
540+
# version, input count, output count, locktime, segwit marker, flag
541+
base_weight = (4 + 1 + 1 + 4) * 4 + 1 + 1
542+
if chainparams['elements']:
543+
# Elements has empty surjection and rangeproof, and fee output
544+
base_weight += 2 * 4 + (32 + 1 + 1 + 1) * 4
545+
# Bech32 change output
546+
change_weight = (8 + 1 + (1 + 1 + 20) + (32 + 1 + 1 + 1)) * 4
547+
else:
548+
# P2TR output
549+
change_weight = (8 + 1 + (1 + 1 + 32)) * 4
550+
551+
# Both minimal and higher feerate
552+
for feerate in (253, 1000):
553+
# Try with both 1 and 2 inputs
554+
for amount, num_inputs in ((260, 1), (5000, 2)):
555+
prep = l1.rpc.fundpsbt(amount, f"{feerate}perkw", base_weight, excess_as_change=True)
556+
assert prep['estimated_final_weight'] == base_weight + change_weight + input_weight * num_inputs
557+
signed = l1.rpc.signpsbt(prep['psbt'])['signed_psbt']
558+
sent = l1.rpc.sendpsbt(signed)
559+
txinfo = bitcoind.rpc.getmempoolentry(sent['txid'])
560+
assert txinfo['weight'] == prep['estimated_final_weight']
561+
# We never actually added that `amount` output to PSBT, so that appears as "fee"
562+
fee = int(txinfo['fees']['base'] * 100_000_000) - amount
563+
actual_feerate = fee / (txinfo['weight'] / 1000)
564+
# Out by one errors (due to rounding) change feerate by 2.
565+
assert feerate - 2 < actual_feerate < feerate + 2
566+
448567

449568
def test_reserveinputs(node_factory, bitcoind, chainparams):
450569
amount = 1000000

0 commit comments

Comments
 (0)