from fixtures import * # noqa: F401,F403 from decimal import Decimal from pyln.client import Millisatoshi from fixtures import TEST_NETWORK from utils import ( sync_blockheight, wait_for, only_one ) import os import pytest import unittest def find_tags(evs, tag): return [e for e in evs if e['tag'] == tag] def find_first_tag(evs, tag): ev = find_tags(evs, tag) assert len(ev) > 0 return ev[0] @pytest.mark.developer("dev-ignore-htlcs") @unittest.skipIf(TEST_NETWORK != 'regtest', "fixme: broadcast fails, dusty") def test_bookkeeping_closing_trimmed_htlcs(node_factory, bitcoind, executor): l1, l2 = node_factory.line_graph(2) # Send l2 funds via the channel l1.pay(l2, 11000000) l1.rpc.dev_ignore_htlcs(id=l2.info['id'], ignore=True) # This will get stuck due to l3 ignoring htlcs executor.submit(l2.pay, l1, 100001) l1.daemon.wait_for_log('their htlc 0 dev_ignore_htlcs') l1.rpc.dev_fail(l2.info['id']) l1.wait_for_channel_onchain(l2.info['id']) bitcoind.generate_block(1) l1.daemon.wait_for_log(' to ONCHAIN') l2.daemon.wait_for_log(' to ONCHAIN') bitcoind.generate_block(5) sync_blockheight(bitcoind, [l1]) l1.daemon.wait_for_log('Broadcasting OUR_DELAYED_RETURN_TO_WALLET') bitcoind.generate_block(20) sync_blockheight(bitcoind, [l1]) l1.daemon.wait_for_log(r'All outputs resolved.*') evs = l1.rpc.listaccountevents()['events'] close = find_first_tag(evs, 'channel_close') delayed_to = find_first_tag(evs, 'delayed_to_us') # find the chain fee entry for the channel close fees = find_tags(evs, 'onchain_fee') close_fee = [e for e in fees if e['txid'] == close['txid']] assert len(close_fee) == 1 assert Millisatoshi(close_fee[0]['credit']) + Millisatoshi(delayed_to['credit']) == Millisatoshi(close['debit']) # l2's fees should equal the trimmed htlc out evs = l2.rpc.listaccountevents()['events'] close = find_first_tag(evs, 'channel_close') deposit = find_first_tag(evs, 'deposit') fees = find_tags(evs, 'onchain_fee') close_fee = [e for e in fees if e['txid'] == close['txid']] assert len(close_fee) == 1 # sent htlc was too small, we lose it, rounded up to nearest sat assert close_fee[0]['credit'] == '101000msat' assert Millisatoshi(close_fee[0]['credit']) + Millisatoshi(deposit['credit']) == Millisatoshi(close['debit']) @unittest.skipIf(TEST_NETWORK != 'regtest', "fixme: broadcast fails, dusty") def test_bookkeeping_closing_subsat_htlcs(node_factory, bitcoind, chainparams): """Test closing balances when HTLCs are: sub 1-satoshi""" l1, l2 = node_factory.line_graph(2) l1.pay(l2, 111) l1.pay(l2, 222) l1.pay(l2, 4000000) l2.stop() l1.rpc.close(l2.info['id'], 1) bitcoind.generate_block(5, wait_for_mempool=1) l2.start() sync_blockheight(bitcoind, [l1]) l1.daemon.wait_for_log('Broadcasting OUR_DELAYED_RETURN_TO_WALLET') bitcoind.generate_block(80) sync_blockheight(bitcoind, [l1, l2]) evs = l1.rpc.listaccountevents()['events'] # check that closing equals onchain deposits + fees close = find_first_tag(evs, 'channel_close') delayed_to = find_first_tag(evs, 'delayed_to_us') fees = find_tags(evs, 'onchain_fee') close_fee = [e for e in fees if e['txid'] == close['txid']] assert len(close_fee) == 1 assert Millisatoshi(close_fee[0]['credit']) + Millisatoshi(delayed_to['credit']) == Millisatoshi(close['debit']) evs = l2.rpc.listaccountevents()['events'] close = find_first_tag(evs, 'channel_close') deposit = find_first_tag(evs, 'deposit') fees = find_tags(evs, 'onchain_fee') close_fee = [e for e in fees if e['txid'] == close['txid']] assert len(close_fee) == 1 # too small to fit, we lose them as miner fees assert close_fee[0]['credit'] == '333msat' assert Millisatoshi(close_fee[0]['credit']) + Millisatoshi(deposit['credit']) == Millisatoshi(close['debit']) @unittest.skipIf(TEST_NETWORK != 'regtest', "External wallet support doesn't work with elements yet.") def test_bookkeeping_external_withdraws(node_factory, bitcoind): """ Withdrawals to an external address shouldn't be included in the income statements until confirmed""" l1 = node_factory.get_node() addr = l1.rpc.newaddr()['bech32'] amount = 1111111 amount_msat = Millisatoshi(amount * 1000) bitcoind.rpc.sendtoaddress(addr, amount / 10**8) bitcoind.rpc.sendtoaddress(addr, amount / 10**8) bitcoind.generate_block(1) wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 2) waddr = l1.bitcoin.rpc.getnewaddress() # Ok, now we send some funds to an external address out = l1.rpc.withdraw(waddr, amount // 2) # Make sure bitcoind received the withdrawal unspent = l1.bitcoin.rpc.listunspent(0) withdrawal = [u for u in unspent if u['txid'] == out['txid']] assert withdrawal[0]['amount'] == Decimal('0.00555555') incomes = l1.rpc.listincome()['income_events'] # There should only be two income events: deposits to wallet # for {amount} assert len(incomes) == 2 for inc in incomes: assert inc['account'] == 'wallet' assert inc['tag'] == 'deposit' assert Millisatoshi(inc['credit']) == amount_msat # The event should show up in the 'listaccountevents' however events = l1.rpc.listaccountevents()['events'] assert len(events) == 3 external = [e for e in events if e['account'] == 'external'][0] assert Millisatoshi(external['credit']) == Millisatoshi(amount // 2 * 1000) btc_balance = only_one(only_one(l1.rpc.listbalances()['accounts'])['balances']) assert Millisatoshi(btc_balance['balance']) == amount_msat * 2 # Restart the node, issues a balance snapshot # If we were counting these incorrectly, # we'd have a new journal_entry l1.restart() # the number of account + income events should be unchanged incomes = l1.rpc.listincome()['income_events'] assert len(find_tags(incomes, 'journal_entry')) == 0 assert len(incomes) == 2 events = l1.rpc.listaccountevents()['events'] assert len(events) == 3 assert len(find_tags(events, 'journal_entry')) == 0 # the wallet balance should be unchanged btc_balance = only_one(only_one(l1.rpc.listbalances()['accounts'])['balances']) assert Millisatoshi(btc_balance['balance']) == amount_msat * 2 # ok now we mine a block bitcoind.generate_block(1) sync_blockheight(bitcoind, [l1]) # expect the withdrawal to appear in the incomes # and there should be an onchain fee incomes = l1.rpc.listincome()['income_events'] # 2 wallet deposits, 1 wallet withdrawal, 1 onchain_fee assert len(incomes) == 4 withdraw_amt = Millisatoshi(find_tags(incomes, 'withdrawal')[0]['debit']) assert withdraw_amt == Millisatoshi(amount // 2 * 1000) fee_events = find_tags(incomes, 'onchain_fee') assert len(fee_events) == 1 fees = Millisatoshi(fee_events[0]['debit']) # wallet balance is decremented now btc_balance = only_one(only_one(l1.rpc.listbalances()['accounts'])['balances']) assert Millisatoshi(btc_balance['balance']) == amount_msat * 2 - withdraw_amt - fees @unittest.skipIf(TEST_NETWORK != 'regtest', "External wallet support doesn't work with elements yet.") @unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "Depends on sqlite3 database location") def test_bookkeeping_external_withdraw_missing(node_factory, bitcoind): """ Withdrawals to an external address turn up as extremely large onchain_fees when they happen before our accounting plugin is attached""" l1 = node_factory.get_node() basedir = l1.daemon.opts.get("lightning-dir") addr = l1.rpc.newaddr()['bech32'] amount = 1111111 amount_msat = Millisatoshi(amount * 1000) bitcoind.rpc.sendtoaddress(addr, amount / 10**8) bitcoind.rpc.sendtoaddress(addr, amount / 10**8) bitcoind.generate_block(1) wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 2) waddr = l1.bitcoin.rpc.getnewaddress() # Ok, now we send some funds to an external address l1.rpc.withdraw(waddr, amount // 2) # There should only be two income events: deposits to wallet assert len(l1.rpc.listincome()['income_events']) == 2 # There are three account events: 2 wallet deposits, 1 external deposit assert len(l1.rpc.listaccountevents()['events']) == 3 # Stop node and remove the accounts data l1.stop() os.remove(os.path.join(basedir, TEST_NETWORK, 'accounts.sqlite3')) l1.start() # the number of income events should be unchanged assert len(l1.rpc.listincome()['income_events']) == 2 # we're now missing the external deposit events = l1.rpc.listaccountevents()['events'] assert len(events) == 2 assert len([e for e in events if e['account'] == 'external']) == 0 assert len(find_tags(events, 'journal_entry')) == 0 # the wallet balance should be unchanged btc_balance = only_one(only_one(l1.rpc.listbalances()['accounts'])['balances']) assert Millisatoshi(btc_balance['balance']) == amount_msat * 2 # ok now we mine a block bitcoind.generate_block(1) sync_blockheight(bitcoind, [l1]) # expect the withdrawal to appear in the incomes # and there should be an onchain fee incomes = l1.rpc.listincome()['income_events'] # 2 wallet deposits, 1 onchain_fee assert len(incomes) == 3 assert len(find_tags(incomes, 'withdrawal')) == 0 fee_events = find_tags(incomes, 'onchain_fee') assert len(fee_events) == 1 fees = Millisatoshi(fee_events[0]['debit']) assert fees > Millisatoshi(amount // 2 * 1000) # wallet balance is decremented now bal = only_one(only_one(l1.rpc.listbalances()['accounts'])['balances']) assert Millisatoshi(bal['balance']) == amount_msat * 2 - fees @unittest.skipIf(TEST_NETWORK != 'regtest', "External wallet support doesn't work with elements yet.") def test_bookkeeping_rbf_withdraw(node_factory, bitcoind): """ If a withdraw to an external gets RBF'd, it should *not* show up in our income ever. (but it will show up in our account events) """ l1 = node_factory.get_node() addr = l1.rpc.newaddr()['bech32'] amount = 1111111 bitcoind.rpc.sendtoaddress(addr, amount / 10**8) bitcoind.generate_block(1) wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 1) assert len(l1.rpc.listaccountevents()['events']) == 1 assert len(l1.rpc.listincome()['income_events']) == 1 # Ok, now we send some funds to an external address waddr = l1.bitcoin.rpc.getnewaddress() out1 = l1.rpc.withdraw(waddr, amount // 2, feerate='253perkw') mempool = bitcoind.rpc.getrawmempool(True) assert len(list(mempool.keys())) == 1 assert out1['txid'] in list(mempool.keys()) # another account event, still one income event assert len(l1.rpc.listaccountevents()['events']) == 2 assert len(l1.rpc.listincome()['income_events']) == 1 # unreserve the existing output l1.rpc.unreserveinputs(out1['psbt'], 200) # resend the tx out2 = l1.rpc.withdraw(waddr, amount // 2, feerate='1000perkw') mempool = bitcoind.rpc.getrawmempool(True) assert len(list(mempool.keys())) == 1 assert out2['txid'] in list(mempool.keys()) # another account event, still one income event assert len(l1.rpc.listaccountevents()['events']) == 3 assert len(l1.rpc.listincome()['income_events']) == 1 # ok now we mine a block bitcoind.generate_block(1) sync_blockheight(bitcoind, [l1]) acct_evs = l1.rpc.listaccountevents()['events'] externs = [e for e in acct_evs if e['account'] == 'external'] assert len(externs) == 2 assert externs[0]['outpoint'][:-2] == out1['txid'] assert externs[0]['blockheight'] == 0 assert externs[1]['outpoint'][:-2] == out2['txid'] assert externs[1]['blockheight'] > 0 withdraws = find_tags(l1.rpc.listincome()['income_events'], 'withdrawal') assert len(withdraws) == 1 assert withdraws[0]['outpoint'][:-2] == out2['txid'] # make sure no onchain fees are counted for the replaced tx fees = find_tags(acct_evs, 'onchain_fee') assert len(fees) > 1 for fee in fees: assert fee['txid'] == out2['txid'] fees = find_tags(l1.rpc.listincome(consolidate_fees=False)['income_events'], 'onchain_fee') assert len(fees) == 2 fees = find_tags(l1.rpc.listincome(consolidate_fees=True)['income_events'], 'onchain_fee') assert len(fees) == 1