balance-snaps: add a balance_snapshot event; fires after first catchup

Fire off a snapshot of current account balances (node wallet + every
'active' channel) after we've caught up to the chain tip for the *first*
time (in other words, on start).
This commit is contained in:
niftynei
2021-12-10 09:46:42 -06:00
committed by Rusty Russell
parent f169597a02
commit 70a73928cb
6 changed files with 109 additions and 7 deletions

View File

@@ -791,6 +791,47 @@ at the time lightningd broadcasts the notification.
`coin_type` is the BIP173 name for the coin which moved.
### `balance_snapshot`
Emitted after we've caught up to the chain head on first start. Lists all
current accounts (`account_id` matches the `account_id` emitted from
`coin_movement`). Useful for checkpointing account balances.
```json
{
"balance_snapshots": [
{
'node_id': '035d2b1192dfba134e10e540875d366ebc8bc353d5aa766b80c090b39c3a5d885d',
'blockheight': 101,
'timestamp': 1639076327,
'accounts': [
{
'account_id': 'wallet',
'balance': '0msat',
'coin_type': 'bcrt'
}
]
},
{
'node_id': '035d2b1192dfba134e10e540875d366ebc8bc353d5aa766b80c090b39c3a5d885d',
'blockheight': 110,
'timestamp': 1639076343,
'accounts': [
{
'account_id': 'wallet',
'balance': '995433000msat',
'coin_type': 'bcrt'
}, {
'account_id': '5b65c199ee862f49758603a5a29081912c8816a7c0243d1667489d244d3d055f',
'balance': '500000000msat',
'coin_type': 'bcrt'
}
]
}
]
}
```
### `openchannel_peer_sigs`
When opening a channel with a peer using the collaborative transaction protocol

View File

@@ -27,6 +27,8 @@
/* Mutual recursion via timer. */
static void try_extend_tip(struct chain_topology *topo);
static bool first_update_complete = false;
/* init_topo sets topo->root, start_fee_estimate clears
* feerate_uninitialized (even if unsuccessful) */
static void maybe_completed_init(struct chain_topology *topo)
@@ -631,8 +633,10 @@ static void updates_complete(struct chain_topology *topo)
topo->prev_tip = topo->tip->blkid;
/* Send out an account balance snapshot */
/* FIXME: only issue on first updates_complete call */
send_account_balance_snapshot(topo->ld, topo->tip->height);
if (!first_update_complete) {
send_account_balance_snapshot(topo->ld, topo->tip->height);
first_update_complete = true;
}
}
/* If bitcoind is synced, we're now synced. */

View File

@@ -100,6 +100,7 @@ void send_account_balance_snapshot(struct lightningd *ld, u32 blockheight)
/* Add the 'wallet' account balance */
snap->accts = tal_arr(snap, struct account_balance *, 1);
bal = tal(snap, struct account_balance);
bal->balance = AMOUNT_MSAT(0);
bal->acct_id = WALLET;
bal->bip173_name = chainparams->lightning_hrp;

30
tests/plugins/balance_snaps.py Executable file
View File

@@ -0,0 +1,30 @@
#!/usr/bin/env python3
from pyln.client import Plugin
import json
import os.path
plugin = Plugin()
@plugin.subscribe("balance_snapshot")
def notify_balance_snapshot(plugin, balance_snapshot, **kwargs):
# we save to disk so that we don't get borked if the node restarts
# assumes notification calls are synchronous (not thread safe)
with open('snaps.json', 'a') as f:
f.write(json.dumps(balance_snapshot) + ',')
@plugin.method('listsnapshots')
def return_moves(plugin):
result = []
if os.path.exists('snaps.json'):
with open('snaps.json', 'r') as f:
jd = f.read()
result = json.loads('[' + jd[:-1] + ']')
return {'balance_snapshots': result}
plugin.run()

View File

@@ -7,7 +7,8 @@ from utils import (
only_one, sync_blockheight, wait_for, TIMEOUT,
account_balance, first_channel_id, closing_fee, TEST_NETWORK,
scriptpubkey_addr, calc_lease_fee, EXPERIMENTAL_FEATURES,
check_utxos_channel, anchor_expected, check_coin_moves
check_utxos_channel, anchor_expected, check_coin_moves,
check_balance_snaps
)
import os
@@ -1038,12 +1039,15 @@ def test_channel_lease_lessor_cheat(node_factory, bitcoind, chainparams):
'''
Check that lessee can recover funds if lessor cheats
'''
balance_snaps = os.path.join(os.getcwd(), 'tests/plugins/balance_snaps.py')
opts = [{'funder-policy': 'match', 'funder-policy-mod': 100,
'lease-fee-base-msat': '100sat', 'lease-fee-basis': 100,
'may_reconnect': True, 'allow_warning': True},
'may_reconnect': True, 'allow_warning': True,
'plugin': balance_snaps},
{'funder-policy': 'match', 'funder-policy-mod': 100,
'lease-fee-base-msat': '100sat', 'lease-fee-basis': 100,
'may_reconnect': True, 'allow_broken_log': True}]
'may_reconnect': True, 'allow_broken_log': True,
'plugin': balance_snaps}]
l1, l2, = node_factory.get_nodes(2, opts=opts)
amount = 500000
feerate = 2000
@@ -1203,17 +1207,18 @@ def test_penalty_htlc_tx_fulfill(node_factory, bitcoind, chainparams):
# We track channel balances, to verify that accounting is ok.
coin_mvt_plugin = os.path.join(os.getcwd(), 'tests/plugins/coin_movements.py')
balance_snaps = os.path.join(os.getcwd(), 'tests/plugins/balance_snaps.py')
l1, l2, l3, l4 = node_factory.line_graph(4,
opts=[{'disconnect': ['-WIRE_UPDATE_FULFILL_HTLC'],
'may_reconnect': True,
'dev-no-reconnect': None},
{'plugin': coin_mvt_plugin,
{'plugin': [coin_mvt_plugin, balance_snaps],
'disable-mpp': None,
'dev-no-reconnect': None,
'may_reconnect': True,
'allow_broken_log': True},
{'plugin': coin_mvt_plugin,
{'plugin': [coin_mvt_plugin, balance_snaps],
'dev-no-reconnect': None,
'may_reconnect': True,
'allow_broken_log': True},
@@ -1331,6 +1336,17 @@ def test_penalty_htlc_tx_fulfill(node_factory, bitcoind, chainparams):
tags = check_utxos_channel(l2, [channel_id], expected_2, filter_channel=channel_id)
check_utxos_channel(l3, [channel_id], expected_3, tags, filter_channel=channel_id)
if not chainparams['elements']:
# Also check snapshots
expected_bals_2 = [
{'blockheight': 101, 'accounts': [{'balance': '0msat'}]},
{'blockheight': 108, 'accounts': [{'balance': '995433000msat'}, {'balance': '500000000msat'}, {'balance': '499994999msat'}]},
# There's a duplicate because we stop and restart l2 twice
# (both times at block 108)
{'blockheight': 108, 'accounts': [{'balance': '995433000msat'}, {'balance': '500000000msat'}, {'balance': '499994999msat'}]},
]
check_balance_snaps(l2, expected_bals_2)
@pytest.mark.developer("needs DEVELOPER=1")
@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "Makes use of the sqlite3 db")

View File

@@ -96,6 +96,16 @@ def calc_lease_fee(amt, feerate, rates):
return fee
def check_balance_snaps(n, expected_bals):
snaps = n.rpc.listsnapshots()['balance_snapshots']
for snap, exp in zip(snaps, expected_bals):
assert snap['blockheight'] == exp['blockheight']
for acct, exp_acct in zip(snap['accounts'], exp['accounts']):
# FIXME: also check 'account_id's (these change every run)
for item in ['balance']:
assert acct[item] == exp_acct[item]
def check_coin_moves(n, account_id, expected_moves, chainparams):
moves = n.rpc.call('listcoinmoves_plugin')['coin_moves']
node_id = n.info['id']