bkpr: add bookkeeping assertions to splice in + out tests

Make sure that the fees and channel balances for splice ins and outs
work as expected.

Note this is for a single-sided splice.
Changelog-None: Tests!
This commit is contained in:
niftynei 2024-08-06 00:18:28 -05:00 committed by Rusty Russell
parent fcebb33180
commit cb2163e439
2 changed files with 154 additions and 20 deletions

View File

@ -1,7 +1,10 @@
from fixtures import * # noqa: F401,F403
from pathlib import Path
from pyln.client import Millisatoshi
import pytest
import unittest
from utils import (
bkpr_account_balance, check_coin_moves, first_channel_id,
TEST_NETWORK, only_one, wait_for
)
@ -9,61 +12,186 @@ from utils import (
@pytest.mark.openchannel('v1')
@pytest.mark.openchannel('v2')
@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need')
def test_script_splice_out(node_factory, bitcoind):
l1, l2 = node_factory.line_graph(2, fundamount=1000000, wait_for_announce=True, opts={'experimental-splicing': None})
def test_script_splice_out(node_factory, bitcoind, chainparams):
fundamt = 1000000
coin_mvt_plugin = Path(__file__).parent / "plugins" / "coin_movements.py"
l1, l2 = node_factory.line_graph(2, fundamount=fundamt, wait_for_announce=True,
opts={'experimental-splicing': None,
'plugin': coin_mvt_plugin})
initial_wallet_balance = Millisatoshi(bkpr_account_balance(l1, 'wallet'))
initial_channel_balance = Millisatoshi(bkpr_account_balance(l1, first_channel_id(l1, l2)))
assert initial_channel_balance == Millisatoshi(fundamt * 1000)
# Splice out 100k from first channel, explicitly putting result less fees into onchain wallet
l1.rpc.splice("*:? -> 100000; 100%-fee -> wallet", force_feerate=True, debug_log=True)
spliceamt = 100000
l1.rpc.splice(f"*:? -> {spliceamt}; 100%-fee -> wallet", force_feerate=True, debug_log=True)
p1 = only_one(l1.rpc.listpeerchannels(peer_id=l2.info['id'])['channels'])
p2 = only_one(l2.rpc.listpeerchannels(l1.info['id'])['channels'])
assert p1['inflight'][0]['splice_amount'] == -100000
assert p1['inflight'][0]['total_funding_msat'] == 900000000
assert p1['inflight'][0]['our_funding_msat'] == 1000000000
assert p1['inflight'][0]['splice_amount'] == -1 * spliceamt
assert p1['inflight'][0]['total_funding_msat'] == (fundamt - spliceamt) * 1000
assert p1['inflight'][0]['our_funding_msat'] == fundamt * 1000
assert p2['inflight'][0]['splice_amount'] == 0
assert p2['inflight'][0]['total_funding_msat'] == 900000000
assert p2['inflight'][0]['total_funding_msat'] == (fundamt - spliceamt) * 1000
assert p2['inflight'][0]['our_funding_msat'] == 0
bitcoind.generate_block(6, wait_for_mempool=1)
l2.daemon.wait_for_log(r'lightningd, splice_locked clearing inflights')
p1 = only_one(l1.rpc.listpeerchannels(peer_id=l2.info['id'])['channels'])
p2 = only_one(l2.rpc.listpeerchannels(l1.info['id'])['channels'])
assert p1['to_us_msat'] == 900000000
assert p1['total_msat'] == 900000000
assert p1['to_us_msat'] == (fundamt - spliceamt) * 1000
assert p1['total_msat'] == (fundamt - spliceamt) * 1000
assert p2['to_us_msat'] == 0
assert p2['total_msat'] == 900000000
assert p2['total_msat'] == (fundamt - spliceamt) * 1000
assert 'inflight' not in p1
assert 'inflight' not in p2
wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 2)
wait_for(lambda: len(l1.rpc.listfunds()['channels']) == 1)
# At the end we'd expect the balance of channel 1 to be down by the splice amount
end_channel_balance = Millisatoshi(bkpr_account_balance(l1, first_channel_id(l1, l2)))
end_wallet_balance = Millisatoshi(bkpr_account_balance(l1, 'wallet'))
assert initial_channel_balance - Millisatoshi(spliceamt * 1000) == end_channel_balance
# The fee is assumed to be the difference between the start+end balances?
fee_guess = initial_wallet_balance + initial_channel_balance - end_channel_balance - end_wallet_balance
# We'd expect the following coin movements
starting_wallet_msat = 2000000000
expected_wallet_moves = [
# initial deposit
{'type': 'chain_mvt', 'credit_msat': starting_wallet_msat, 'debit_msat': 0, 'tags': ['deposit']},
# channel open spend
{'type': 'chain_mvt', 'credit_msat': 0, 'debit_msat': starting_wallet_msat, 'tags': ['withdrawal']},
# channel open change
{'type': 'chain_mvt', 'credit_msat': initial_wallet_balance, 'debit_msat': 0, 'tags': ['deposit']},
# deposit of spliceamt - fees
{'type': 'chain_mvt', 'credit_msat': Millisatoshi(spliceamt * 1000) - fee_guess, 'debit_msat': 0, 'tags': ['deposit']},
]
check_coin_moves(l1, 'wallet', expected_wallet_moves, chainparams)
expected_channel_moves = [
# channel_open [utxo created], chain_mvt (fundamt - spliceamt)
{'type': 'chain_mvt', 'credit_msat': fundamt * 1000, 'debit_msat': 0, 'tags': ['channel_open', 'opener']},
# channel_close [utxo spend], chain_mvt (fundamt)
{'type': 'chain_mvt', 'debit_msat': fundamt * 1000, 'credit_msat': 0, 'tags': ['channel_close', 'splice']},
# channel_open [utxo created], chain_mvt (fundamt - spliceamt)
{'type': 'chain_mvt', 'credit_msat': (fundamt - spliceamt) * 1000, 'debit_msat': 0, 'tags': ['channel_open', 'opener']},
]
check_coin_moves(l1, first_channel_id(l1, l2), expected_channel_moves, chainparams)
# Make sure the channel isn't marked as closed in bookkeeper
account_id = first_channel_id(l1, l2)
account_info = only_one([acct for acct in l1.rpc.bkpr_listbalances()['accounts'] if acct['account'] == account_id])
assert not account_info['account_closed']
# We'd also expect the wallet to be up by splice amt - fees
onchain_fees = [fee for fee in l1.rpc.bkpr_listincome()['income_events'] if fee['tag'] == 'onchain_fee']
assert len(onchain_fees) == 2
total_fees = sum([x['debit_msat'] for x in onchain_fees])
assert starting_wallet_msat == end_wallet_balance + total_fees + end_channel_balance
# Now close the channel and check that everything resolves as expected
l1.rpc.close(l2.info['id'])
l1.wait_for_channel_onchain(l2.info['id'])
account_info = only_one([acct for acct in l1.rpc.bkpr_listbalances()['accounts'] if acct['account'] == account_id])
assert not account_info['account_closed']
@pytest.mark.openchannel('v1')
@pytest.mark.openchannel('v2')
@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need')
def test_script_splice_in(node_factory, bitcoind):
l1, l2 = node_factory.line_graph(2, fundamount=1000000, wait_for_announce=True, opts={'experimental-splicing': None})
def test_script_splice_in(node_factory, bitcoind, chainparams):
fundamt = 1000000
coin_mvt_plugin = Path(__file__).parent / "plugins" / "coin_movements.py"
l1, l2 = node_factory.line_graph(2, fundamount=fundamt, wait_for_announce=True,
opts={'experimental-splicing': None,
'plugin': coin_mvt_plugin})
initial_wallet_balance = Millisatoshi(bkpr_account_balance(l1, 'wallet'))
initial_channel_balance = Millisatoshi(bkpr_account_balance(l1, first_channel_id(l1, l2)))
assert initial_channel_balance == Millisatoshi(fundamt * 1000)
# Splice in 100k sats into first channel, explicitly taking out 200k sats from wallet
# and letting change go automatically back to wallet (100k less onchain fees)
l1.rpc.splice("wallet -> 200000; 100000 -> *:?", force_feerate=True, debug_log=True)
spliceamt = 100000
withdraw_amt = 200000
starting_wallet_msat = withdraw_amt * 10000
l1.rpc.splice(f"wallet -> {withdraw_amt}; {spliceamt} -> *:?", force_feerate=True, debug_log=True)
p1 = only_one(l1.rpc.listpeerchannels(peer_id=l2.info['id'])['channels'])
p2 = only_one(l2.rpc.listpeerchannels(l1.info['id'])['channels'])
assert p1['inflight'][0]['splice_amount'] == 100000
assert p1['inflight'][0]['total_funding_msat'] == 1100000000
assert p1['inflight'][0]['our_funding_msat'] == 1000000000
assert p1['inflight'][0]['splice_amount'] == spliceamt
assert p1['inflight'][0]['total_funding_msat'] == (fundamt + spliceamt) * 1000
assert p1['inflight'][0]['our_funding_msat'] == fundamt * 1000
assert p2['inflight'][0]['splice_amount'] == 0
assert p2['inflight'][0]['total_funding_msat'] == 1100000000
assert p2['inflight'][0]['total_funding_msat'] == (fundamt + spliceamt) * 1000
assert p2['inflight'][0]['our_funding_msat'] == 0
bitcoind.generate_block(6, wait_for_mempool=1)
l2.daemon.wait_for_log(r'lightningd, splice_locked clearing inflights')
p1 = only_one(l1.rpc.listpeerchannels(peer_id=l2.info['id'])['channels'])
p2 = only_one(l2.rpc.listpeerchannels(l1.info['id'])['channels'])
assert p1['to_us_msat'] == 1100000000
assert p1['total_msat'] == 1100000000
assert p1['to_us_msat'] == (fundamt + spliceamt) * 1000
assert p1['total_msat'] == (fundamt + spliceamt) * 1000
assert p2['to_us_msat'] == 0
assert p2['total_msat'] == 1100000000
assert p2['total_msat'] == (fundamt + spliceamt) * 1000
assert 'inflight' not in p1
assert 'inflight' not in p2
wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 1)
wait_for(lambda: len(l1.rpc.listfunds()['channels']) == 1)
# At the end we'd expect the balance of channel 1 to be up by the splice amount
end_channel_balance = Millisatoshi(bkpr_account_balance(l1, first_channel_id(l1, l2)))
end_wallet_balance = Millisatoshi(bkpr_account_balance(l1, 'wallet'))
assert initial_channel_balance + Millisatoshi(spliceamt * 1000) == end_channel_balance
# The fee is assumed to be the difference between the start+end balances?
fee_guess = initial_wallet_balance + initial_channel_balance - end_channel_balance - end_wallet_balance
# We'd expect the following coin movements
expected_wallet_moves = [
# initial deposit
{'type': 'chain_mvt', 'credit_msat': starting_wallet_msat, 'debit_msat': 0, 'tags': ['deposit']},
# channel open spend
{'type': 'chain_mvt', 'credit_msat': 0, 'debit_msat': starting_wallet_msat, 'tags': ['withdrawal']},
# channel open change
{'type': 'chain_mvt', 'credit_msat': initial_wallet_balance, 'debit_msat': 0, 'tags': ['deposit']},
# splice-in spend
{'type': 'chain_mvt', 'debit_msat': initial_wallet_balance, 'credit_msat': 0, 'tags': ['withdrawal']},
# post-splice deposit
{'type': 'chain_mvt', 'credit_msat': initial_wallet_balance - Millisatoshi(spliceamt * 1000) - fee_guess, 'debit_msat': 0, 'tags': ['deposit']},
]
check_coin_moves(l1, 'wallet', expected_wallet_moves, chainparams)
expected_channel_moves = [
# channel_open [utxo created], chain_mvt
{'type': 'chain_mvt', 'credit_msat': fundamt * 1000, 'debit_msat': 0, 'tags': ['channel_open', 'opener']},
# channel_close [utxo spend], chain_mvt (fundamt)
{'type': 'chain_mvt', 'debit_msat': fundamt * 1000, 'credit_msat': 0, 'tags': ['channel_close', 'splice']},
# channel_open [utxo created], chain_mvt (fundamt - spliceamt)
{'type': 'chain_mvt', 'credit_msat': (fundamt + spliceamt) * 1000, 'debit_msat': 0, 'tags': ['channel_open', 'opener']},
]
check_coin_moves(l1, first_channel_id(l1, l2), expected_channel_moves, chainparams)
# Make sure the channel isn't marked as closed in bookkeeper
account_id = first_channel_id(l1, l2)
account_info = only_one([acct for acct in l1.rpc.bkpr_listbalances()['accounts'] if acct['account'] == account_id])
assert not account_info['account_closed']
# We'd also expect the wallet to be down by splice amt + fees
onchain_fees = [fee for fee in l1.rpc.bkpr_listincome()['income_events'] if fee['tag'] == 'onchain_fee']
assert len(onchain_fees) == 2
total_fees = sum([x['debit_msat'] for x in onchain_fees])
assert starting_wallet_msat == end_wallet_balance + total_fees + end_channel_balance
# Now close the channel and check that everything resolves as expected
l1.rpc.close(l2.info['id'])
l1.wait_for_channel_onchain(l2.info['id'])
account_info = only_one([acct for acct in l1.rpc.bkpr_listbalances()['accounts'] if acct['account'] == account_id])
assert not account_info['account_closed']

View File

@ -74,6 +74,12 @@ def expected_channel_features(extra=[]):
return hex_bits(features + extra)
def bkpr_account_balance(node, acct_id):
balances = node.rpc.bkpr_listbalances()['accounts']
acct = only_one([acct for acct in balances if acct['account'] == acct_id])
return only_one(acct['balances'])['balance_msat']
def move_matches(exp, mv):
if mv['type'] != exp['type']:
return False