Files
palladum-lightning/tests/test_bookkeeper.py
Rusty Russell 9e8e85de99 pytest: allow pushed after onchain_fee in test_bookkeeping_missed_chans_pushed
It can happen, and it's perfectly reasonable.  If this happens in other places, we might need to allow
arbitrary reordering?

```
2026-01-29T05:55:58.5474967Z         exp_events = [{'tag': 'channel_open', 'credit_msat': open_amt * 1000, 'debit_msat': 0},
2026-01-29T05:55:58.5475765Z                       {'tag': 'pushed', 'credit_msat': 0, 'debit_msat': push_amt},
2026-01-29T05:55:58.5476454Z                       {'tag': 'onchain_fee', 'credit_msat': 4927000, 'debit_msat': 0},
2026-01-29T05:55:58.5477168Z                       {'tag': 'invoice', 'credit_msat': 0, 'debit_msat': invoice_msat}]
2026-01-29T05:55:58.5477797Z >       check_events(l1, channel_id, exp_events)
2026-01-29T05:55:58.5478120Z 
2026-01-29T05:55:58.5478282Z tests/test_bookkeeper.py:402: 
2026-01-29T05:55:58.5478777Z _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
2026-01-29T05:55:58.5479162Z 
2026-01-29T05:55:58.5479396Z node = <fixtures.LightningNode object at 0x7fa3660a6140>
2026-01-29T05:55:58.5480158Z channel_id = 'a4e913b2d143efc3d90cfa66a56aeed3eb9e1533b350c8e84124bdec37bcf74a'
2026-01-29T05:55:58.5481929Z exp_events = [{'credit_msat': 10000000000, 'debit_msat': 0, 'tag': 'channel_open'}, {'credit_msat': 0, 'debit_msat': 1000000000, 'tag': 'pushed'}, {'credit_msat': 4927000, 'debit_msat': 0, 'tag': 'onchain_fee'}, {'credit_msat': 0, 'debit_msat': 11000000, 'tag': 'invoice'}]
2026-01-29T05:55:58.5483442Z 
2026-01-29T05:55:58.5483671Z     def check_events(node, channel_id, exp_events):
2026-01-29T05:55:58.5484551Z         chan_events = [ev for ev in node.rpc.bkpr_listaccountevents()['events'] if ev['account'] == channel_id]
2026-01-29T05:55:58.5485684Z         stripped = [{k: d[k] for k in ('tag', 'credit_msat', 'debit_msat') if k in d} for d in chan_events]
2026-01-29T05:55:58.5486455Z >       assert stripped == exp_events
2026-01-29T05:55:58.5489277Z E       AssertionError: assert [{'tag': 'channel_open', 'credit_msat': 10000000000, 'debit_msat': 0}, {'tag': 'onchain_fee', 'credit_msat': 4927000, 'debit_msat': 0}, {'tag': 'pushed', 'credit_msat': 0, 'debit_msat': 1000000000}, {'tag': 'invoice', 'credit_msat': 0, 'debit_msat': 11000000}] == [{'tag': 'channel_open', 'credit_msat': 10000000000, 'debit_msat': 0}, {'tag': 'pushed', 'credit_msat': 0, 'debit_msat': 1000000000}, {'tag': 'onchain_fee', 'credit_msat': 4927000, 'debit_msat': 0}, {'tag': 'invoice', 'credit_msat': 0, 'debit_msat': 11000000}]
2026-01-29T05:55:58.5492021Z E         
2026-01-29T05:55:58.5492767Z E         At index 1 diff: {'tag': 'onchain_fee', 'credit_msat': 4927000, 'debit_msat': 0} != {'tag': 'pushed', 'credit_msat': 0, 'debit_msat': 1000000000}
2026-01-29T05:55:58.5493812Z E         
2026-01-29T05:55:58.5494078Z E         Full diff:
2026-01-29T05:55:58.5494373Z E           [
2026-01-29T05:55:58.5494863Z E               {
2026-01-29T05:55:58.5495166Z E                   'credit_msat': 10000000000,
2026-01-29T05:55:58.5495565Z E                   'debit_msat': 0,
2026-01-29T05:55:58.5495946Z E                   'tag': 'channel_open',
2026-01-29T05:55:58.5496330Z E         -     },
2026-01-29T05:55:58.5496613Z E         -     {
2026-01-29T05:55:58.5496906Z E         -         'credit_msat': 0,
2026-01-29T05:55:58.5497285Z E         -         'debit_msat': 1000000000,
2026-01-29T05:55:58.5497900Z E         -         'tag': 'pushed',
2026-01-29T05:55:58.5498264Z E               },
2026-01-29T05:55:58.5498531Z E               {
2026-01-29T05:55:58.5498818Z E                   'credit_msat': 4927000,
2026-01-29T05:55:58.5499200Z E                   'debit_msat': 0,
2026-01-29T05:55:58.5499563Z E                   'tag': 'onchain_fee',
2026-01-29T05:55:58.5499925Z E               },
2026-01-29T05:55:58.5500190Z E               {
2026-01-29T05:55:58.5500477Z E                   'credit_msat': 0,
2026-01-29T05:55:58.5500863Z E         +         'debit_msat': 1000000000,
2026-01-29T05:55:58.5501255Z E         +         'tag': 'pushed',
2026-01-29T05:55:58.5501592Z E         +     },
2026-01-29T05:55:58.5501853Z E         +     {
2026-01-29T05:55:58.5502141Z E         +         'credit_msat': 0,
2026-01-29T05:55:58.5502511Z E                   'debit_msat': 11000000,
2026-01-29T05:55:58.5502889Z E                   'tag': 'invoice',
2026-01-29T05:55:58.5503424Z E               },
2026-01-29T05:55:58.5503698Z E           ]
2026-01-29T05:55:58.5503861Z 
2026-01-29T05:55:58.5504027Z tests/test_bookkeeper.py:29: AssertionError
```

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
2026-02-03 16:12:04 +10:30

1228 lines
54 KiB
Python

from fixtures import * # noqa: F401,F403
from decimal import Decimal
from pyln.client import Millisatoshi, RpcError
from fixtures import TEST_NETWORK
from utils import (
sync_blockheight, wait_for, only_one, first_channel_id, TIMEOUT
)
from pathlib import Path
import os
import pytest
import time
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]
def check_events(node, channel_id, exp_events, alt_events=None):
chan_events = [ev for ev in node.rpc.bkpr_listaccountevents()['events'] if ev['account'] == channel_id]
stripped = [{k: d[k] for k in ('tag', 'credit_msat', 'debit_msat') if k in d} for d in chan_events]
assert stripped == exp_events or stripped == alt_events
@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, opts={'old_hsmsecret': True})
# 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')
_, txid, blocks = l1.wait_for_onchaind_tx('OUR_DELAYED_RETURN_TO_WALLET',
'OUR_UNILATERAL/DELAYED_OUTPUT_TO_US')
assert blocks == 4
bitcoind.generate_block(4)
bitcoind.generate_block(20, wait_for_mempool=txid)
sync_blockheight(bitcoind, [l1])
l1.daemon.wait_for_log(r'All outputs resolved.*')
evs = l1.rpc.bkpr_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 close_fee[0]['credit_msat'] + delayed_to['credit_msat'] == close['debit_msat']
# l2's fees should equal the trimmed htlc out
evs = l2.rpc.bkpr_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_msat'] == Millisatoshi('101000msat')
assert close_fee[0]['credit_msat'] + deposit['credit_msat'] == close['debit_msat']
@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, opts={'old_hsmsecret': True})
l1.pay(l2, 111)
l1.pay(l2, 222)
l1.pay(l2, 4000000)
# Make sure l2 bookkeeper processes event before we stop it!
wait_for(lambda: len([e for e in l2.rpc.bkpr_listaccountevents()['events'] if e['tag'] == 'invoice']) == 3)
l2.stop()
l1.rpc.close(l2.info['id'], 1)
bitcoind.generate_block(1, wait_for_mempool=1)
_, txid, blocks = l1.wait_for_onchaind_tx('OUR_DELAYED_RETURN_TO_WALLET',
'OUR_UNILATERAL/DELAYED_OUTPUT_TO_US')
assert blocks == 4
bitcoind.generate_block(4)
l2.start()
bitcoind.generate_block(80, wait_for_mempool=txid)
sync_blockheight(bitcoind, [l1, l2])
evs = l1.rpc.bkpr_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 close_fee[0]['credit_msat'] + delayed_to['credit_msat'] == close['debit_msat']
evs = l2.rpc.bkpr_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_msat'] == Millisatoshi('333msat')
assert close_fee[0]['credit_msat'] + deposit['credit_msat'] == close['debit_msat']
@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()['p2tr']
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.bkpr_listincome()['income_events']
# There are 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 inc['credit_msat'] == amount_msat
# The event should show up in the 'bkpr_listaccountevents' however
events = l1.rpc.bkpr_listaccountevents()['events']
assert len(events) == 3
external = [e for e in events if e['account'] == 'external'][0]
assert external['credit_msat'] == Millisatoshi(amount // 2 * 1000)
btc_balance = only_one(only_one(l1.rpc.bkpr_listbalances()['accounts'])['balances'])
assert btc_balance['balance_msat'] == 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.bkpr_listincome()['income_events']
assert len(find_tags(incomes, 'journal_entry')) == 0
assert len(incomes) == 2
events = l1.rpc.bkpr_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.bkpr_listbalances()['accounts'])['balances'])
assert btc_balance['balance_msat'] == 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.bkpr_listincome()['income_events']
# 2 wallet deposits, 1 wallet withdrawal, 1 onchain_fee
assert len(incomes) == 4
withdraw_amt = find_tags(incomes, 'withdrawal')[0]['debit_msat']
assert withdraw_amt == Millisatoshi(amount // 2 * 1000)
fee_events = find_tags(incomes, 'onchain_fee')
assert len(fee_events) == 1
fees = fee_events[0]['debit_msat']
# wallet balance is decremented now
btc_balance = only_one(only_one(l1.rpc.bkpr_listbalances()['accounts'])['balances'])
assert btc_balance['balance_msat'] == amount_msat * 2 - withdraw_amt - 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()['p2tr']
amount = 1111111
event_counter = 0
income_counter = 0
bitcoind.rpc.sendtoaddress(addr, amount / 10**8)
event_counter += 1
income_counter += 1
bitcoind.generate_block(1)
wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 1)
assert len(l1.rpc.bkpr_listaccountevents()['events']) == event_counter
assert len(l1.rpc.bkpr_listincome()['income_events']) == income_counter
# Ok, now we send some funds to an external address
waddr = l1.bitcoin.rpc.getnewaddress()
out1 = l1.rpc.withdraw(waddr, amount // 2, feerate='253perkw')
event_counter += 1
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.bkpr_listaccountevents()['events']) == event_counter
assert len(l1.rpc.bkpr_listincome()['income_events']) == income_counter
# 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)
event_counter += 1
assert len(list(mempool.keys())) == 1
assert out2['txid'] in list(mempool.keys())
# another account event, still one income event
assert len(l1.rpc.bkpr_listaccountevents()['events']) == event_counter
assert len(l1.rpc.bkpr_listincome()['income_events']) == income_counter
# ok now we mine a block
bitcoind.generate_block(1)
sync_blockheight(bitcoind, [l1])
acct_evs = l1.rpc.bkpr_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.bkpr_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.bkpr_listincome(consolidate_fees=False)['income_events'], 'onchain_fee')
assert len(fees) == 2
fees = find_tags(l1.rpc.bkpr_listincome(consolidate_fees=True)['income_events'], 'onchain_fee')
assert len(fees) == 1
@pytest.mark.openchannel('v2')
@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "turns off bookkeeper at start")
@unittest.skipIf(TEST_NETWORK != 'regtest', "network fees hardcoded")
def test_bookkeeping_missed_chans_leases(node_factory, bitcoind):
"""
Test that a lease is correctly recorded if bookkeeper was off
"""
coin_mvt_plugin = Path(__file__).parent / "plugins" / "coin_movements.py"
opts = {'funder-policy': 'match', 'funder-policy-mod': 100,
'lease-fee-base-sat': '100sat', 'lease-fee-basis': 100,
'plugin': str(coin_mvt_plugin),
'disable-plugin': 'bookkeeper'}
l1, l2 = node_factory.get_nodes(2, opts=opts)
open_amt = 500000
feerate = 2000
lease_fee = 6268000
invoice_msat = 11000000
l1.fundwallet(open_amt * 1000)
l2.fundwallet(open_amt * 1000)
l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
# l1 leases a channel from l2
compact_lease = l2.rpc.funderupdate()['compact_lease']
txid = l1.rpc.fundchannel(l2.info['id'], open_amt, request_amt=open_amt,
feerate='{}perkw'.format(feerate),
compact_lease=compact_lease)['txid']
bitcoind.generate_block(1, wait_for_mempool=[txid])
wait_for(lambda: l1.channel_state(l2) == 'CHANNELD_NORMAL')
scid = l1.get_channel_scid(l2)
l1.wait_local_channel_active(scid)
channel_id = first_channel_id(l1, l2)
# Sigh. bookkeeper sorts events by timestamp. If the invoice event happens
# too close, it can change the order, so sleep here.
time.sleep(2)
# Send l2 funds via the channel
l1.pay(l2, invoice_msat)
# Make sure they're completely settled, so accounting correct.
wait_for(lambda: only_one(l1.rpc.listpeerchannels()['channels'])['htlcs'] == [])
# Now turn the bookkeeper on and restart
l1.stop()
l2.stop()
del l1.daemon.opts['disable-plugin']
del l2.daemon.opts['disable-plugin']
l1.start()
l2.start()
# l1 events: nothing missed!
exp_events = [{'tag': 'channel_open', 'credit_msat': open_amt * 1000 + lease_fee, 'debit_msat': 0},
{'tag': 'lease_fee', 'credit_msat': 0, 'debit_msat': lease_fee},
{'tag': 'onchain_fee', 'credit_msat': 1314000, 'debit_msat': 0},
{'tag': 'invoice', 'credit_msat': 0, 'debit_msat': invoice_msat}]
check_events(l1, channel_id, exp_events)
exp_events = [{'tag': 'channel_open', 'credit_msat': open_amt * 1000, 'debit_msat': 0},
{'tag': 'lease_fee', 'credit_msat': lease_fee, 'debit_msat': 0},
{'tag': 'onchain_fee', 'credit_msat': 894000, 'debit_msat': 0},
{'tag': 'invoice', 'credit_msat': invoice_msat, 'debit_msat': 0}]
check_events(l2, channel_id, exp_events)
@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "turns off bookkeeper at start")
@unittest.skipIf(TEST_NETWORK != 'regtest', "network fees hardcoded")
@pytest.mark.openchannel('v1', 'Uses push-msat')
def test_bookkeeping_missed_chans_pushed(node_factory, bitcoind):
"""
Test for a push_msat value in a missed channel open.
"""
coin_mvt_plugin = Path(__file__).parent / "plugins" / "coin_movements.py"
l1, l2 = node_factory.get_nodes(2, opts={'disable-plugin': 'bookkeeper',
'plugin': str(coin_mvt_plugin)})
# Double check there's no bookkeeper plugin on
assert l1.daemon.opts['disable-plugin'] == 'bookkeeper'
assert l2.daemon.opts['disable-plugin'] == 'bookkeeper'
open_amt = 10**7
push_amt = 10**6 * 1000
invoice_msat = 11000000
l1.fundwallet(200000000)
l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
txid = l1.rpc.fundchannel(l2.info['id'], open_amt, push_msat=push_amt)['txid']
bitcoind.generate_block(1, wait_for_mempool=[txid])
wait_for(lambda: l1.channel_state(l2) == 'CHANNELD_NORMAL')
scid = l1.get_channel_scid(l2)
l1.wait_local_channel_active(scid)
channel_id = first_channel_id(l1, l2)
# Sigh. bookkeeper sorts events by timestamp. If the invoice event happens
# too close, it can change the order, so sleep here.
time.sleep(1)
# Send l2 funds via the channel
l1.pay(l2, invoice_msat)
# Make sure they're completely settled, so accounting correct.
wait_for(lambda: only_one(l1.rpc.listpeerchannels()['channels'])['htlcs'] == [])
# Now turn the bookkeeper on and restart
l1.stop()
l2.stop()
del l1.daemon.opts['disable-plugin']
del l2.daemon.opts['disable-plugin']
l1.start()
l2.start()
# l1 events
exp_events = [{'tag': 'channel_open', 'credit_msat': open_amt * 1000, 'debit_msat': 0},
{'tag': 'pushed', 'credit_msat': 0, 'debit_msat': push_amt},
{'tag': 'onchain_fee', 'credit_msat': 4927000, 'debit_msat': 0},
{'tag': 'invoice', 'credit_msat': 0, 'debit_msat': invoice_msat}]
# We sometimes see onchain_fee first:
alt_events = [{'tag': 'channel_open', 'credit_msat': open_amt * 1000, 'debit_msat': 0},
{'tag': 'onchain_fee', 'credit_msat': 4927000, 'debit_msat': 0},
{'tag': 'pushed', 'credit_msat': 0, 'debit_msat': push_amt},
{'tag': 'invoice', 'credit_msat': 0, 'debit_msat': invoice_msat}]
check_events(l1, channel_id, exp_events, alt_events)
# l2 events
exp_events = [{'tag': 'channel_open', 'credit_msat': 0, 'debit_msat': 0},
{'tag': 'pushed', 'credit_msat': push_amt, 'debit_msat': 0},
{'tag': 'invoice', 'credit_msat': invoice_msat, 'debit_msat': 0}]
check_events(l2, channel_id, exp_events)
@unittest.skipIf(TEST_NETWORK != 'regtest', "network fees hardcoded")
@pytest.mark.openchannel('v1')
def test_bookkeeping_inspect_multifundchannel(node_factory, bitcoind):
"""
Test that bookkeeper splits multifundchannel fees correctly for single funded channels.
For single funded channels, l1 pays the entirety of the fee associated with multifundchannel, and the fee is
split into each channel and is viewed from the opener's perspective.
"""
l1, l2, l3, l4 = node_factory.get_nodes(4)
l1.fundwallet(200000000)
l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
l1.rpc.connect(l3.info['id'], 'localhost', l3.port)
l1.rpc.connect(l4.info['id'], 'localhost', l4.port)
destinations = [{"id": '{}@localhost:{}'.format(l2.info['id'], l2.port),
"amount": 25000},
{"id": '{}@localhost:{}'.format(l3.info['id'], l3.port),
"amount": 25000},
{"id": '{}@localhost:{}'.format(l4.info['id'], l4.port),
"amount": 25000}]
multifundchannel_return = l1.rpc.multifundchannel(destinations)
multifundchannel_txid = multifundchannel_return['txid']
channel_ids = multifundchannel_return['channel_ids']
channel_12_channel_id = channel_ids[0]['channel_id']
channel_13_channel_id = channel_ids[1]['channel_id']
channel_14_channel_id = channel_ids[2]['channel_id']
bitcoind.generate_block(1, wait_for_mempool=[multifundchannel_txid])
wait_for(lambda: l1.channel_state(l2) == 'CHANNELD_NORMAL')
wait_for(lambda: l1.channel_state(l3) == 'CHANNELD_NORMAL')
wait_for(lambda: l1.channel_state(l4) == 'CHANNELD_NORMAL')
# now use getblock to get the tx fee from bitcoin-cli's perspective
multifundchannel_rawtx = l1.bitcoin.rpc.getrawtransaction(multifundchannel_txid, True)
blockhash = multifundchannel_rawtx['blockhash']
getblock_tx = l1.bitcoin.rpc.getblock(blockhash, 2)['tx']
getblock_fee_btc = 0
for tx in getblock_tx:
if tx['txid'] == multifundchannel_txid:
getblock_fee_btc = tx['fee']
# now sum bookkeeper fees for each channel to get the total fees for this tx
channel_12_multifundchannel_fee_msat = l1.rpc.bkpr_inspect(channel_12_channel_id)['txs'][0]['fees_paid_msat']
channel_13_multifundchannel_fee_msat = l1.rpc.bkpr_inspect(channel_13_channel_id)['txs'][0]['fees_paid_msat']
channel_14_multifundchannel_fee_msat = l1.rpc.bkpr_inspect(channel_14_channel_id)['txs'][0]['fees_paid_msat']
bkpr_total_fee_msat = (channel_12_multifundchannel_fee_msat
+ channel_13_multifundchannel_fee_msat
+ channel_14_multifundchannel_fee_msat)
assert bkpr_total_fee_msat == int(getblock_fee_btc * 100000000000)
@unittest.skipIf(TEST_NETWORK != 'regtest', "network fees hardcoded")
@pytest.mark.openchannel('v2')
def test_bookkeeping_inspect_mfc_dual_funded(node_factory, bitcoind):
"""
Test that bookkeeper splits multifundchannel fees correctly for dual funded channels.
For dual funded channels, the other nodes also pay part of the fees associated with multifundchannel, since they
are also funding the channel. To calculate the total fees spent for the multifundchannel tx, the
other nodes' fees paid must be included.
"""
opts = {'experimental-dual-fund': None, 'funder-policy': 'match',
'funder-policy-mod': 100, 'funder-lease-requests-only': False}
l1, l2, l3, l4 = node_factory.get_nodes(4, opts=opts)
l1.fundwallet(2000000000)
l2.fundwallet(2000000000)
l3.fundwallet(2000000000)
l4.fundwallet(2000000000)
l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
l1.rpc.connect(l3.info['id'], 'localhost', l3.port)
l1.rpc.connect(l4.info['id'], 'localhost', l4.port)
destinations = [{"id": '{}@localhost:{}'.format(l2.info['id'], l2.port),
"amount": 25000,
"announce": True},
{"id": '{}@localhost:{}'.format(l3.info['id'], l3.port),
"amount": 25000,
"announce": True},
{"id": '{}@localhost:{}'.format(l4.info['id'], l4.port),
"amount": 25000,
"announce": True}]
multifundchannel_return = l1.rpc.multifundchannel(destinations)
multifundchannel_txid = multifundchannel_return['txid']
channel_ids = multifundchannel_return['channel_ids']
channel_12_channel_id = channel_ids[0]['channel_id']
channel_13_channel_id = channel_ids[1]['channel_id']
channel_14_channel_id = channel_ids[2]['channel_id']
bitcoind.generate_block(5, wait_for_mempool=[multifundchannel_txid])
wait_for(lambda: l1.channel_state(l2) == 'CHANNELD_NORMAL')
wait_for(lambda: l1.channel_state(l3) == 'CHANNELD_NORMAL')
wait_for(lambda: l1.channel_state(l4) == 'CHANNELD_NORMAL')
# now use getblock to get the tx fee from bitcoin-cli's perspective
multifundchannel_rawtx = l1.bitcoin.rpc.getrawtransaction(multifundchannel_txid, True)
blockhash = multifundchannel_rawtx['blockhash']
getblock_tx = l1.bitcoin.rpc.getblock(blockhash, 2)['tx']
getblock_fee_btc = 0
for tx in getblock_tx:
if tx['txid'] == multifundchannel_txid:
getblock_fee_btc = tx['fee']
# now sum bookkeeper fees for each node to get the total fees for this tx
channel_12_multifundchannel_fee_msat = l1.rpc.bkpr_inspect(channel_12_channel_id)['txs'][0]['fees_paid_msat']
channel_21_multifundchannel_fee_msat = l2.rpc.bkpr_inspect(channel_12_channel_id)['txs'][0]['fees_paid_msat']
channel_13_multifundchannel_fee_msat = l1.rpc.bkpr_inspect(channel_13_channel_id)['txs'][0]['fees_paid_msat']
channel_31_multifundchannel_fee_msat = l3.rpc.bkpr_inspect(channel_13_channel_id)['txs'][0]['fees_paid_msat']
channel_14_multifundchannel_fee_msat = l1.rpc.bkpr_inspect(channel_14_channel_id)['txs'][0]['fees_paid_msat']
channel_41_multifundchannel_fee_msat = l4.rpc.bkpr_inspect(channel_14_channel_id)['txs'][0]['fees_paid_msat']
bkpr_total_fee_msat = (channel_12_multifundchannel_fee_msat
+ channel_21_multifundchannel_fee_msat
+ channel_13_multifundchannel_fee_msat
+ channel_31_multifundchannel_fee_msat
+ channel_14_multifundchannel_fee_msat
+ channel_41_multifundchannel_fee_msat)
assert bkpr_total_fee_msat == int(getblock_fee_btc * 100000000000)
@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "turns off bookkeeper at start")
@unittest.skipIf(TEST_NETWORK != 'regtest', "network fees hardcoded")
@pytest.mark.openchannel('v1', 'Uses push-msat')
def test_bookkeeping_missed_chans_pay_after(node_factory, bitcoind):
"""
Route a payment through a channel that we didn't have open when the bookkeeper
was around
"""
coin_mvt_plugin = Path(__file__).parent / "plugins" / "coin_movements.py"
l1, l2 = node_factory.get_nodes(2, opts={'disable-plugin': 'bookkeeper',
'may_reconnect': True,
'plugin': str(coin_mvt_plugin)})
# Double check there's no bookkeeper plugin on
assert l1.daemon.opts['disable-plugin'] == 'bookkeeper'
assert l2.daemon.opts['disable-plugin'] == 'bookkeeper'
open_amt = 10**7
invoice_msat = 11000000
l1.fundwallet(200000000)
l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
txid = l1.rpc.fundchannel(l2.info['id'], open_amt)['txid']
bitcoind.generate_block(1, wait_for_mempool=[txid])
wait_for(lambda: l1.channel_state(l2) == 'CHANNELD_NORMAL')
scid = l1.get_channel_scid(l2)
l1.wait_local_channel_active(scid)
channel_id = first_channel_id(l1, l2)
# Now turn the bookkeeper on and restart
l1.stop()
l2.stop()
del l1.daemon.opts['disable-plugin']
del l2.daemon.opts['disable-plugin']
l1.start()
l2.start()
# Should have channel in both, with balances
for n in [l1, l2]:
accts = [ba['account'] for ba in n.rpc.bkpr_listbalances()['accounts']]
assert channel_id in accts
# Send a payment, should be ok.
l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
l1.wait_local_channel_active(scid)
l1.pay(l2, invoice_msat)
l1.daemon.wait_for_log(r'coin movement:.*\'invoice\'')
# l1 events
exp_events = [{'tag': 'channel_open', 'credit_msat': open_amt * 1000, 'debit_msat': 0},
{'tag': 'onchain_fee', 'credit_msat': 4927000, 'debit_msat': 0},
{'tag': 'invoice', 'credit_msat': 0, 'debit_msat': invoice_msat}]
check_events(l1, channel_id, exp_events)
# l2 events
exp_events = [{'tag': 'channel_open', 'credit_msat': 0, 'debit_msat': 0},
{'tag': 'invoice', 'credit_msat': invoice_msat, 'debit_msat': 0}]
check_events(l2, channel_id, exp_events)
@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "turns off bookkeeper at start")
def test_bookkeeping_onchaind_txs(node_factory, bitcoind):
"""
Test for a channel that's closed, but whose close tx
re-appears in the rescan
"""
coin_mvt_plugin = Path(__file__).parent / "plugins" / "coin_movements.py"
l1, l2 = node_factory.line_graph(2,
wait_for_announce=True,
opts={'disable-plugin': 'bookkeeper',
'plugin': str(coin_mvt_plugin)})
# Double check there's no bookkeeper plugin on
assert l1.daemon.opts['disable-plugin'] == 'bookkeeper'
# Send l2 funds via the channel
l1.pay(l2, 11000000)
l1.daemon.wait_for_log(r'coin movement:.*\'invoice\'')
bitcoind.generate_block(10)
# Amicably close the channel, mine 101 blocks (channel forgotten)
l1.rpc.close(l2.info['id'])
l1.wait_for_channel_onchain(l2.info['id'])
bitcoind.generate_block(101)
sync_blockheight(bitcoind, [l1])
l1.daemon.wait_for_log('onchaind complete, forgetting peer')
# Now turn the bookkeeper on and restart
l1.stop()
del l1.daemon.opts['disable-plugin']
# Roll back -- close is picked up for a forgotten channel
l1.daemon.opts['rescan'] = 102
l1.start()
# We should have everything.
events = l1.rpc.bkpr_listaccountevents()['events']
assert len(events) == 12
wallet_bal = only_one([a for a in l1.rpc.bkpr_listbalances()['accounts'] if a['account'] == 'wallet'])
funds = l1.rpc.listfunds()
assert len(funds['channels']) == 0
outs = sum([out['amount_msat'] for out in funds['outputs']])
assert outs == only_one(wallet_bal['balances'])['balance_msat']
def test_bookkeeping_descriptions(node_factory, bitcoind, chainparams):
"""
When an 'invoice' type event comes through, we look up the description details
to include about the item. Particularly useful for CSV outputs etc.
"""
l1, l2 = node_factory.line_graph(2)
# Send l2 funds via the channel
bolt11_desc = 'test "bolt11" description, 🥰🪢'
l1.pay(l2, 11000000, label=bolt11_desc)
wait_for(lambda: only_one(l1.rpc.listpeerchannels()['channels'])['htlcs'] == [])
# Need to call bookkeeper to trigger analysis!
l1_inc_ev = l1.rpc.bkpr_listincome()['income_events']
l1.daemon.wait_for_log('coin_move .* [(]invoice[)] 0msat -11000000msat')
l2.rpc.bkpr_listincome()
l2.daemon.wait_for_log('coin_move .* [(]invoice[)] 11000000msat')
# Test paying an bolt11 invoice (rcvr)
inv = only_one([ev for ev in l1_inc_ev if ev['tag'] == 'invoice'])
assert inv['description'] == bolt11_desc
# Test paying an bolt11 invoice (sender)
l2_inc_ev = l2.rpc.bkpr_listincome()['income_events']
inv = only_one([ev for ev in l2_inc_ev if ev['tag'] == 'invoice'])
assert inv['description'] == bolt11_desc
# Make an offer (l1)
bolt12_desc = 'test "bolt12" description, 🥰🪢'
offer = l1.rpc.call('offer', [100, bolt12_desc])
invoice = l2.rpc.call('fetchinvoice', {'offer': offer['bolt12']})
paid = l2.rpc.pay(invoice['invoice'])
wait_for(lambda: only_one(l1.rpc.listpeerchannels()['channels'])['htlcs'] == [])
wait_for(lambda: only_one(l2.rpc.listpeerchannels()['channels'])['htlcs'] == [])
l1_inc_ev = l1.rpc.bkpr_listincome()['income_events']
l1.daemon.wait_for_log('coin_move .* [(]invoice[)] 100msat')
l2.rpc.bkpr_listincome()
l2.daemon.wait_for_log('coin_move .* [(]invoice[)] 0msat -100msat')
# Test paying an offer (bolt12) (rcvr)
inv = only_one([ev for ev in l1_inc_ev if 'payment_id' in ev and ev['payment_id'] == paid['payment_hash']])
assert inv['description'] == bolt12_desc
# Test paying an offer (bolt12) (sender)
l2_inc_ev = l2.rpc.bkpr_listincome()['income_events']
inv = only_one([ev for ev in l2_inc_ev if 'payment_id' in ev and ev['payment_id'] == paid['payment_hash'] and ev['tag'] == 'invoice'])
assert inv['description'] == bolt12_desc
# Check the CSVs look groovy
l1.rpc.bkpr_dumpincomecsv('koinly', 'koinly.csv')
l2.rpc.bkpr_dumpincomecsv('koinly', 'koinly.csv')
koinly_path = os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, 'koinly.csv')
l1_koinly_csv = Path(koinly_path).read_bytes()
bolt11_exp = bytes('invoice,"test \'bolt11\' description, 🥰🪢",', 'utf-8')
bolt12_exp = bytes('invoice,"test \'bolt12\' description, 🥰🪢",', 'utf-8')
assert l1_koinly_csv.find(bolt11_exp) >= 0
assert l1_koinly_csv.find(bolt12_exp) >= 0
koinly_path = os.path.join(l2.daemon.lightning_dir, TEST_NETWORK, 'koinly.csv')
l2_koinly_csv = Path(koinly_path).read_bytes()
assert l2_koinly_csv.find(bolt11_exp) >= 0
assert l2_koinly_csv.find(bolt12_exp) >= 0
# Test that we can update the description, payment id
edited_desc_payid = 'edited payment_id description'
for node in [l1, l2]:
results = node.rpc.bkpr_editdescriptionbypaymentid(paid['payment_hash'], edited_desc_payid)
assert only_one(results['updated'])['description'] == edited_desc_payid
# Test that we can update the description, outpoint
edited_desc_outpoint = 'edited outpoint description'
deposits = [ev for ev in l1_inc_ev if ev['tag'] == 'deposit']
assert len(deposits) > 0
results = l1.rpc.bkpr_editdescriptionbyoutpoint(deposits[0]['outpoint'], edited_desc_outpoint)
assert only_one(results['updated'])['description'] == edited_desc_outpoint
# Test that input that doesn't match an event returns empty list
fake_outpoint = '01' * 32 + ':100'
results = l1.rpc.bkpr_editdescriptionbyoutpoint(fake_outpoint, edited_desc_outpoint)
assert len(results['updated']) == 0
# Make sure that only one event actually updated
acct_evs = l1.rpc.bkpr_listaccountevents()['events']
income_evs = l1.rpc.bkpr_listincome()['income_events']
for evs in [acct_evs, income_evs]:
assert only_one([ev for ev in evs if 'description' in ev and ev['description'] == edited_desc_payid])
assert only_one([ev for ev in evs if 'description' in ev and ev['description'] == edited_desc_outpoint])
# Test persistence!
l1.restart()
assert l1.rpc.bkpr_listaccountevents()['events'] == acct_evs
assert l1.rpc.bkpr_listincome()['income_events'] == income_evs
def test_empty_node(node_factory, bitcoind):
"""
Make sure that the bookkeeper commands don't blow up
on an empty accounts database.
"""
l1 = node_factory.get_node()
bkpr_cmds = [
('channelsapy', []),
('listaccountevents', []),
('listbalances', []),
('listincome', [])]
for cmd, params in bkpr_cmds:
l1.rpc.call('bkpr-' + cmd, params)
# inspect fails for non-channel accounts
# FIXME: implement for all accounts?
with pytest.raises(RpcError, match=r'not supported for non-channel accounts'):
l1.rpc.bkpr_inspect('wallet')
def test_rebalance_tracking(node_factory, bitcoind):
"""
We identify rebalances (invoices paid and received by our node),
this allows us to filter them out of "incomes" (self-transfers are not income/exp)
and instead only display the cost incurred to move the payment (correctly
marked as a rebalance)
1 -> 2 -> 3 -> 1
"""
rebal_amt = 3210
l1, l2, l3 = node_factory.get_nodes(3)
l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
l2.rpc.connect(l3.info['id'], 'localhost', l3.port)
l3.rpc.connect(l1.info['id'], 'localhost', l1.port)
c12, _ = l1.fundchannel(l2, 10**7, wait_for_active=True)
c23, _ = l2.fundchannel(l3, 10**7, wait_for_active=True)
c31, _ = l3.fundchannel(l1, 10**7, wait_for_active=True)
# Build a rebalance payment
invoice = l1.rpc.invoice(rebal_amt, 'to_self', 'to_self')
pay_hash = invoice['payment_hash']
pay_sec = invoice['payment_secret']
route = [{
'id': l2.info['id'],
'channel': c12,
'direction': int(not l1.info['id'] < l2.info['id']),
'amount_msat': rebal_amt + 1001,
'style': 'tlv',
'delay': 24,
}, {
'id': l3.info['id'],
'channel': c23,
'direction': int(not l2.info['id'] < l3.info['id']),
'amount_msat': rebal_amt + 500,
'style': 'tlv',
'delay': 16,
}, {
'id': l1.info['id'],
'channel': c31,
'direction': int(not l3.info['id'] < l1.info['id']),
'amount_msat': rebal_amt,
'style': 'tlv',
'delay': 8,
}]
l1.rpc.sendpay(route, pay_hash, payment_secret=pay_sec)
result = l1.rpc.waitsendpay(pay_hash, TIMEOUT)
assert result['status'] == 'complete'
wait_for(lambda: 'invoice' not in [ev['tag'] for ev in l1.rpc.bkpr_listincome()['income_events']])
inc_evs = l1.rpc.bkpr_listincome()['income_events']
outbound_chan_id = only_one(l1.rpc.listpeerchannels(l2.info['id'])['channels'])['channel_id']
outbound_ev = only_one([ev for ev in inc_evs if ev['tag'] == 'rebalance_fee'])
assert outbound_ev['account'] == outbound_chan_id
assert outbound_ev['debit_msat'] == Millisatoshi(1001)
assert outbound_ev['credit_msat'] == Millisatoshi(0)
assert outbound_ev['payment_id'] == pay_hash
# Will reload on restart!
l1.restart()
inc_evs = l1.rpc.bkpr_listincome()['income_events']
outbound_chan_id = only_one(l1.rpc.listpeerchannels(l2.info['id'])['channels'])['channel_id']
outbound_ev = only_one([ev for ev in inc_evs if ev['tag'] == 'rebalance_fee'])
assert outbound_ev['account'] == outbound_chan_id
assert outbound_ev['debit_msat'] == Millisatoshi(1001)
assert outbound_ev['credit_msat'] == Millisatoshi(0)
assert outbound_ev['payment_id'] == pay_hash
def test_bookkeeper_custom_notifs(node_factory, chainparams):
# FIXME: what happens if we send internal funds to 'external' wallet?
plugin = os.path.join(
os.path.dirname(__file__), "plugins", "bookkeeper_custom_coins.py"
)
l1, l2 = node_factory.line_graph(2, opts=[{'plugin': plugin}, {}])
outpoint_in = 'aa' * 32 + ':0'
spend_txid = 'bb' * 32
amount = 180000000
withdraw_amt = 55555000
fee = 2000
change_deposit = 'bb' * 32 + ':0'
external_deposit = 'bb' * 32 + ':1'
acct = "nifty's secret stash"
l1.rpc.senddeposit(acct, False, outpoint_in, amount)
l1.daemon.wait_for_log(r"Foreign chain event: deposit \(nifty's secret stash\) 180000000msat -0msat 1679955976 111 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:0")
l1.rpc.sendspend(acct, outpoint_in, spend_txid, amount)
l1.daemon.wait_for_log(r"Foreign chain event: withdrawal \(nifty's secret stash\) 0msat -180000000msat 1679955976 111 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:0 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
# balance should be zero
bals = l1.rpc.bkpr_listbalances()['accounts']
for bal in bals:
if bal['account'] == acct:
# FIXME: how to account for withdraw to external
assert only_one(bal['balances'])['balance_msat'] == Millisatoshi(0)
l1.rpc.senddeposit(acct, False, change_deposit, amount - withdraw_amt - fee)
l1.daemon.wait_for_log(r"Foreign chain event: deposit \(nifty's secret stash\) .* -0msat 1679955976 111 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:0")
# balance should be equal to amount
events = l1.rpc.bkpr_listaccountevents(acct)['events']
for bal in l1.rpc.bkpr_listbalances()['accounts']:
if bal['account'] == acct:
assert only_one(bal['balances'])['balance_msat'] == Millisatoshi(amount - fee - withdraw_amt)
onchain_fee_one = only_one([x['credit_msat'] for x in events if x['type'] == 'onchain_fee'])
assert onchain_fee_one == fee + withdraw_amt
l1.rpc.senddeposit(acct, True, external_deposit, withdraw_amt)
l1.daemon.wait_for_log(r"Foreign chain event: deposit \(external\) .* -0msat 1679955976 111 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:1")
events = l1.rpc.bkpr_listaccountevents(acct)['events']
onchain_fees = [x for x in events if x['type'] == 'onchain_fee']
assert len(onchain_fees) == 2
assert onchain_fees[0]['credit_msat'] == onchain_fee_one
assert onchain_fees[1]['debit_msat'] == withdraw_amt
assert events == [{'account': "nifty's secret stash",
'blockheight': 111,
'credit_msat': 180000000,
'currency': chainparams['bip173_prefix'],
'debit_msat': 0,
'outpoint': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:0',
'tag': 'deposit',
'timestamp': 1679955976,
'type': 'chain'},
{'account': "nifty's secret stash",
'blockheight': 111,
'credit_msat': 0,
'currency': chainparams['bip173_prefix'],
'debit_msat': 180000000,
'outpoint': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:0',
'tag': 'withdrawal',
'timestamp': 1679955976,
'txid': 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
'type': 'chain'},
{'account': "nifty's secret stash",
'blockheight': 111,
'credit_msat': 124443000,
'currency': chainparams['bip173_prefix'],
'debit_msat': 0,
'outpoint': 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:0',
'tag': 'deposit',
'timestamp': 1679955976,
'type': 'chain'},
{'account': "nifty's secret stash",
'credit_msat': 55557000,
'currency': chainparams['bip173_prefix'],
'debit_msat': 0,
'tag': 'onchain_fee',
'timestamp': 1679955976,
'txid': 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
'type': 'onchain_fee'},
{'account': "nifty's secret stash",
'credit_msat': 0,
'currency': chainparams['bip173_prefix'],
'debit_msat': 55555000,
'tag': 'onchain_fee',
'timestamp': 1679955976,
'txid': 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
'type': 'onchain_fee'}]
# This should not blow up
incomes = l1.rpc.bkpr_listincome()['income_events']
acct_fee = only_one([inc['debit_msat'] for inc in incomes if inc['account'] == acct and inc['tag'] == 'onchain_fee'])
assert acct_fee == Millisatoshi(fee)
@unittest.skipIf(TEST_NETWORK != 'regtest', "Snapshots are bitcoin regtest.")
@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "uses snapshots")
def test_migration(node_factory, bitcoind):
generate = False
if generate:
l1, l2 = node_factory.line_graph(2)
l1.pay(l2, 12345678, label="Rusty's payment")
wait_for(lambda: only_one(l1.rpc.listpeerchannels()['channels'])['htlcs'] == [])
wait_for(lambda: only_one(l2.rpc.listpeerchannels()['channels'])['htlcs'] == [])
chan = only_one(l1.rpc.listpeerchannels()['channels'])
# Label change output and funding output
l1.rpc.bkpr_editdescriptionbyoutpoint(f"{chan['funding_txid']}:{chan['funding_outnum']}",
"Rusty's channel")
l1.rpc.bkpr_editdescriptionbyoutpoint(f"{chan['funding_txid']}:{chan['funding_outnum'] ^ 1}",
"Rusty's change")
else:
bitcoind.generate_block(1)
l1 = node_factory.get_node(dbfile="l1-before-moves-in-db.sqlite3.xz",
bkpr_dbfile="l1-bkpr-accounts.sqlite3.xz",
options={'database-upgrade': True},
old_hsmsecret=True)
l2 = node_factory.get_node(dbfile="l2-before-moves-in-db.sqlite3.xz",
bkpr_dbfile="l2-bkpr-accounts.sqlite3.xz",
options={'database-upgrade': True},
old_hsmsecret=True)
chan = only_one(l1.rpc.listpeerchannels()['channels'])
payment = only_one(l1.rpc.listsendpays()['payments'])
events = l1.rpc.bkpr_listaccountevents()['events']
pay_event = only_one([e for e in events if e.get('description') == "Rusty's payment"])
del pay_event['timestamp']
assert pay_event == {'account': chan['channel_id'],
'credit_msat': 0,
'currency': 'bcrt',
'debit_msat': 12345678,
'description': "Rusty's payment",
'is_rebalance': False,
'part_id': 0,
'payment_id': payment['payment_hash'],
'tag': 'invoice',
'type': 'channel'}
open_event = only_one([e for e in events if e.get('description') == "Rusty's channel"])
del open_event['timestamp']
assert open_event == {'account': chan['channel_id'],
'blockheight': 103,
'credit_msat': 1000000000,
'currency': 'bcrt',
'debit_msat': 0,
'description': "Rusty's channel",
'outpoint': f"{chan['funding_txid']}:{chan['funding_outnum']}",
'tag': 'channel_open',
'type': 'chain'}
change_event = only_one([e for e in events if e.get('description') == "Rusty's change"])
del change_event['timestamp']
assert change_event == {'account': 'wallet',
'blockheight': 103,
'credit_msat': 995073000,
'currency': 'bcrt',
'debit_msat': 0,
'description': "Rusty's change",
'outpoint': f"{chan['funding_txid']}:{chan['funding_outnum'] ^ 1}",
'tag': 'deposit',
'type': 'chain'}
# When generating, we want to stop so you can grab databases.
assert generate is False
l1_events = l1.rpc.bkpr_listaccountevents()['events']
for e in l1_events:
del e['timestamp']
l2_events = l2.rpc.bkpr_listaccountevents()['events']
for e in l2_events:
del e['timestamp']
# These were snapshotted before the bkpr migration, so should
# be the same!
assert l1_events == [{'account': 'wallet',
'blockheight': 102,
'credit_msat': 2000000000,
'currency': 'bcrt',
'debit_msat': 0,
'outpoint': '63c59b312976320528552c258ae51563498dfd042b95bb0c842696614d59bb89:1',
'tag': 'deposit',
'type': 'chain'},
{'account': 'wallet',
'blockheight': 103,
'credit_msat': 0,
'currency': 'bcrt',
'debit_msat': 2000000000,
'outpoint': '63c59b312976320528552c258ae51563498dfd042b95bb0c842696614d59bb89:1',
'tag': 'withdrawal',
'txid': '675ab2a8c43afcf98b82a1120d1a4d36768c898792fe1282c5be4ac055377fbe',
'type': 'chain'},
{'account': 'wallet',
'blockheight': 103,
'credit_msat': 995073000,
'currency': 'bcrt',
'debit_msat': 0,
'description': "Rusty's change",
'outpoint': '675ab2a8c43afcf98b82a1120d1a4d36768c898792fe1282c5be4ac055377fbe:1',
'tag': 'deposit',
'type': 'chain'},
{'account': 'be7f3755c04abec58212fe9287898c76364d1a0d12a1828bf9fc3ac4a8b25a67',
'blockheight': 103,
'credit_msat': 1000000000,
'currency': 'bcrt',
'debit_msat': 0,
'description': "Rusty's channel",
'outpoint': '675ab2a8c43afcf98b82a1120d1a4d36768c898792fe1282c5be4ac055377fbe:0',
'tag': 'channel_open',
'type': 'chain'},
{'account': 'be7f3755c04abec58212fe9287898c76364d1a0d12a1828bf9fc3ac4a8b25a67',
'credit_msat': 0,
'currency': 'bcrt',
'debit_msat': 12345678,
'description': "Rusty's payment",
'is_rebalance': False,
'part_id': 0,
'payment_id': '7ccef7e9fabbf4a841af44b1fc7319bc70ce98697b77ce6dacffa84bebcd4350',
'tag': 'invoice',
'type': 'channel'},
{'account': 'be7f3755c04abec58212fe9287898c76364d1a0d12a1828bf9fc3ac4a8b25a67',
'credit_msat': 4927000,
'currency': 'bcrt',
'debit_msat': 0,
'tag': 'onchain_fee',
'txid': '675ab2a8c43afcf98b82a1120d1a4d36768c898792fe1282c5be4ac055377fbe',
'type': 'onchain_fee'},
{'account': 'wallet',
'credit_msat': 1004927000,
'currency': 'bcrt',
'debit_msat': 0,
'tag': 'onchain_fee',
'txid': '675ab2a8c43afcf98b82a1120d1a4d36768c898792fe1282c5be4ac055377fbe',
'type': 'onchain_fee'},
{'account': 'wallet',
'credit_msat': 0,
'currency': 'bcrt',
'debit_msat': 1004927000,
'tag': 'onchain_fee',
'txid': '675ab2a8c43afcf98b82a1120d1a4d36768c898792fe1282c5be4ac055377fbe',
'type': 'onchain_fee'}]
assert l2_events == [{'account': 'be7f3755c04abec58212fe9287898c76364d1a0d12a1828bf9fc3ac4a8b25a67',
'blockheight': 103,
'credit_msat': 0,
'currency': 'bcrt',
'debit_msat': 0,
'outpoint': '675ab2a8c43afcf98b82a1120d1a4d36768c898792fe1282c5be4ac055377fbe:0',
'tag': 'channel_open',
'type': 'chain'},
{'account': 'be7f3755c04abec58212fe9287898c76364d1a0d12a1828bf9fc3ac4a8b25a67',
'credit_msat': 12345678,
'currency': 'bcrt',
'debit_msat': 0,
'description': "Rusty's payment",
'is_rebalance': False,
'part_id': 0,
'payment_id': '7ccef7e9fabbf4a841af44b1fc7319bc70ce98697b77ce6dacffa84bebcd4350',
'tag': 'invoice',
'type': 'channel'}]
@unittest.skipIf(TEST_NETWORK != 'regtest', "Snapshots are bitcoin regtest.")
@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "uses snapshots")
def test_migration_no_bkpr(node_factory, bitcoind):
"""These nodes need to invent coinmoves to make the balances work"""
bitcoind.generate_block(1)
l1 = node_factory.get_node(dbfile="l1-before-moves-in-db.sqlite3.xz",
options={'database-upgrade': True},
old_hsmsecret=True)
l2 = node_factory.get_node(dbfile="l2-before-moves-in-db.sqlite3.xz",
options={'database-upgrade': True},
old_hsmsecret=True)
chan = only_one(l1.rpc.listpeerchannels()['channels'])
l1_events = l1.rpc.bkpr_listaccountevents()['events']
for e in l1_events:
del e['timestamp']
l2_events = l2.rpc.bkpr_listaccountevents()['events']
for e in l2_events:
del e['timestamp']
assert l1_events == [{'account': chan['channel_id'],
'blockheight': 103,
'credit_msat': 1000000000,
'currency': 'bcrt',
'debit_msat': 0,
'outpoint': f"{chan['funding_txid']}:{chan['funding_outnum']}",
'tag': 'channel_open',
'type': 'chain'},
{'account': 'wallet',
'blockheight': 103,
'credit_msat': 995073000,
'currency': 'bcrt',
'debit_msat': 0,
'outpoint': f"{chan['funding_txid']}:{chan['funding_outnum'] ^ 1}",
'tag': 'deposit',
'type': 'chain'},
{'account': chan['channel_id'],
'credit_msat': 0,
'currency': 'bcrt',
'debit_msat': 12345678,
'is_rebalance': False,
'tag': 'journal_entry',
'type': 'channel'}]
assert l2_events == [{'account': chan['channel_id'],
'blockheight': 103,
'credit_msat': 0,
'currency': 'bcrt',
'debit_msat': 0,
'outpoint': f"{chan['funding_txid']}:{chan['funding_outnum']}",
'tag': 'channel_open',
'type': 'chain'},
{'account': chan['channel_id'],
'credit_msat': 12345678,
'currency': 'bcrt',
'debit_msat': 0,
'is_rebalance': False,
'tag': 'journal_entry',
'type': 'channel'}]
@unittest.skipIf(TEST_NETWORK != 'regtest', "External wallet support doesn't work with elements yet.")
def test_listincome_timebox(node_factory, bitcoind):
l1 = node_factory.get_node()
addr = l1.rpc.newaddr()['p2tr']
amount = 1111111
bitcoind.rpc.sendtoaddress(addr, amount / 10**8)
bitcoind.generate_block(1, wait_for_mempool=1)
wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 1)
waddr = bitcoind.rpc.getnewaddress()
# Ok, now we send some funds to an external address, get change.
l1.rpc.withdraw(waddr, amount // 2)
bitcoind.generate_block(1, wait_for_mempool=1)
wait_for(lambda: len(l1.rpc.listfunds(spent=True)['outputs']) == 2)
first_one = int(time.time())
time.sleep(2)
# Do another one, make sure we don't see it if we filter by timestamp.
bitcoind.rpc.sendtoaddress(addr, amount / 10**8)
bitcoind.generate_block(1, wait_for_mempool=1)
wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 2)
incomes = l1.rpc.bkpr_listincome(end_time=first_one)['income_events']
assert [i for i in incomes if i['timestamp'] > first_one] == []
@unittest.skipIf(TEST_NETWORK != 'regtest', "Snapshots are bitcoin regtest.")
@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "uses snapshots")
def test_bkpr_parallel(node_factory, bitcoind, executor):
"""Bookkeeper could crash with parallel requests"""
bitcoind.generate_block(1)
l1 = node_factory.get_node(dbfile="l1-before-moves-in-db.sqlite3.xz",
options={'database-upgrade': True},
old_hsmsecret=True)
fut1 = executor.submit(l1.rpc.bkpr_listincome)
fut2 = executor.submit(l1.rpc.bkpr_listincome)
fut1.result()
fut2.result()
# We save blockheights in storage, so make sure we restore them on restart!
acctevents_before = l1.rpc.bkpr_listaccountevents()
l1.restart()
acctevents_after = l1.rpc.bkpr_listaccountevents()
assert acctevents_after == acctevents_before