From 9c4c0f10fbd2803abcf61c324f94d929d8b48bd2 Mon Sep 17 00:00:00 2001 From: lisa neigut Date: Wed, 18 Mar 2020 18:46:17 -0500 Subject: [PATCH] coin_mvt: add integration tests for in-channel htlc movements --- tests/plugins/coin_movements.py | 39 ++++++++++++ tests/test_plugin.py | 106 +++++++++++++++++++++++++++++++- tests/utils.py | 34 ++++++++++ 3 files changed, 178 insertions(+), 1 deletion(-) create mode 100755 tests/plugins/coin_movements.py diff --git a/tests/plugins/coin_movements.py b/tests/plugins/coin_movements.py new file mode 100755 index 000000000..f633e0111 --- /dev/null +++ b/tests/plugins/coin_movements.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 + +from pyln.client import Plugin + +plugin = Plugin() + + +@plugin.init() +def init(configuration, options, plugin): + plugin.coin_moves = [] + + +@plugin.subscribe("coin_movement") +def notify_coin_movement(plugin, coin_movement, **kwargs): + idx = coin_movement['movement_idx'] + plugin.log("{} coins movement version: {}".format(idx, coin_movement['version'])) + plugin.log("{} coins node: {}".format(idx, coin_movement['node_id'])) + plugin.log("{} coins mvt_type: {}".format(idx, coin_movement['type'])) + plugin.log("{} coins account: {}".format(idx, coin_movement['account_id'])) + plugin.log("{} coins credit: {}".format(idx, coin_movement['credit'])) + plugin.log("{} coins debit: {}".format(idx, coin_movement['debit'])) + plugin.log("{} coins tag: {}".format(idx, coin_movement['tag'])) + plugin.log("{} coins blockheight: {}".format(idx, coin_movement['blockheight'])) + plugin.log("{} coins timestamp: {}".format(idx, coin_movement['timestamp'])) + plugin.log("{} coins unit_of_account: {}".format(idx, coin_movement['unit_of_account'])) + + for f in ['payment_hash', 'utxo_txid', 'vout', 'txid', 'part_id']: + if f in coin_movement: + plugin.log("{} coins {}: {}".format(idx, f, coin_movement[f])) + + plugin.coin_moves.append(coin_movement) + + +@plugin.method('listcoinmoves_plugin') +def return_moves(plugin): + return {'coin_moves': plugin.coin_moves} + + +plugin.run() diff --git a/tests/test_plugin.py b/tests/test_plugin.py index f8a6e5d70..0b760f6bb 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -6,7 +6,8 @@ from pyln.client import RpcError, Millisatoshi from pyln.proto import Invoice from utils import ( DEVELOPER, only_one, sync_blockheight, TIMEOUT, wait_for, TEST_NETWORK, - DEPRECATED_APIS, expected_peer_features, expected_node_features + DEPRECATED_APIS, expected_peer_features, expected_node_features, account_balance, + check_coin_moves, first_channel_id ) import json @@ -1355,3 +1356,106 @@ def test_plugin_fail(node_factory): time.sleep(2) # It should clean up! assert 'failcmd' not in [h['command'] for h in l1.rpc.help()['help']] + + +@unittest.skipIf(not DEVELOPER, "without DEVELOPER=1, gossip v slow") +def test_coin_movement_notices(node_factory, bitcoind): + """Verify that coin movements are triggered correctly. + """ + + l1_l2_mvts = [ + {'type': 'chain_mvt', 'credit': 0, 'debit': 0, 'tag': 'deposit'}, + {'type': 'channel_mvt', 'credit': 100001001, 'debit': 0, 'tag': 'routed'}, + {'type': 'channel_mvt', 'credit': 0, 'debit': 50000000, 'tag': 'routed'}, + {'type': 'channel_mvt', 'credit': 100000000, 'debit': 0, 'tag': 'invoice'}, + {'type': 'channel_mvt', 'credit': 0, 'debit': 50000000, 'tag': 'invoice'}, + {'type': 'chain_mvt', 'credit': 0, 'debit': 1, 'tag': 'chain_fees'}, + {'type': 'chain_mvt', 'credit': 0, 'debit': 100001000, 'tag': 'withdrawal'}, + ] + l2_l3_mvts = [ + {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'}, + {'type': 'channel_mvt', 'credit': 0, 'debit': 100000000, 'tag': 'routed'}, + {'type': 'channel_mvt', 'credit': 50000501, 'debit': 0, 'tag': 'routed'}, + {'type': 'chain_mvt', 'credit': 0, 'debit': 5430501, 'tag': 'chain_fees'}, + {'type': 'chain_mvt', 'credit': 0, 'debit': 944570000, 'tag': 'withdrawal'}, + ] + l2_wallet_mvts = [ + {'type': 'chain_mvt', 'credit': 2000000000, 'debit': 0, 'tag': 'deposit'}, + {'type': 'chain_mvt', 'credit': 0, 'debit': 995418000, 'tag': 'withdrawal'}, + {'type': 'chain_mvt', 'credit': 0, 'debit': 1000000000, 'tag': 'withdrawal'}, + {'type': 'chain_mvt', 'credit': 0, 'debit': 4582000, 'tag': 'chain_fees'}, + {'type': 'chain_mvt', 'credit': 995418000, 'debit': 0, 'tag': 'deposit'}, + {'type': 'chain_mvt', 'credit': 100001000, 'debit': 0, 'tag': 'deposit'}, + {'type': 'chain_mvt', 'credit': 944570000, 'debit': 0, 'tag': 'deposit'}, + ] + + l1, l2, l3 = node_factory.line_graph(3, opts=[ + {}, + {'plugin': os.path.join(os.getcwd(), 'tests/plugins/coin_movements.py')}, + {} + ], wait_for_announce=True) + + bitcoind.generate_block(5) + wait_for(lambda: len(l1.rpc.listchannels()['channels']) == 4) + amount = 10**8 + + payment_hash13 = l3.rpc.invoice(amount, "first", "desc")['payment_hash'] + route = l1.rpc.getroute(l3.info['id'], amount, 1)['route'] + + # status: offered -> settled + l1.rpc.sendpay(route, payment_hash13) + l1.rpc.waitsendpay(payment_hash13) + + # status: offered -> failed + route = l1.rpc.getroute(l3.info['id'], amount, 1)['route'] + payment_hash13 = "f" * 64 + with pytest.raises(RpcError): + l1.rpc.sendpay(route, payment_hash13) + l1.rpc.waitsendpay(payment_hash13) + + # go the other direction + payment_hash31 = l1.rpc.invoice(amount // 2, "first", "desc")['payment_hash'] + route = l3.rpc.getroute(l1.info['id'], amount // 2, 1)['route'] + l3.rpc.sendpay(route, payment_hash31) + l3.rpc.waitsendpay(payment_hash31) + + # receive a payment (endpoint) + payment_hash12 = l2.rpc.invoice(amount, "first", "desc")['payment_hash'] + route = l1.rpc.getroute(l2.info['id'], amount, 1)['route'] + l1.rpc.sendpay(route, payment_hash12) + l1.rpc.waitsendpay(payment_hash12) + + # send a payment (originator) + payment_hash21 = l1.rpc.invoice(amount // 2, "second", "desc")['payment_hash'] + route = l2.rpc.getroute(l1.info['id'], amount // 2, 1)['route'] + l2.rpc.sendpay(route, payment_hash21) + l2.rpc.waitsendpay(payment_hash21) + + # close the channel down + chan1 = l2.get_channel_scid(l1) + chan3 = l2.get_channel_scid(l3) + chanid_1 = first_channel_id(l2, l1) + chanid_3 = first_channel_id(l2, l3) + + l2.rpc.close(chan1) + l2.daemon.wait_for_log(' to CLOSINGD_SIGEXCHANGE') + assert account_balance(l2, chanid_1) == 100001001 + bitcoind.generate_block(6) + sync_blockheight(bitcoind, [l2]) + l2.daemon.wait_for_log('{}.*FUNDING_TRANSACTION/FUNDING_OUTPUT->MUTUAL_CLOSE depth'.format(l1.info['id'])) + + l2.rpc.close(chan3) + l2.daemon.wait_for_log(' to CLOSINGD_SIGEXCHANGE') + assert account_balance(l2, chanid_3) == 950000501 + bitcoind.generate_block(6) + sync_blockheight(bitcoind, [l2]) + l2.daemon.wait_for_log('{}.*FUNDING_TRANSACTION/FUNDING_OUTPUT->MUTUAL_CLOSE depth'.format(l3.info['id'])) + + # Ending channel balance should be zero + assert account_balance(l2, chanid_1) == 0 + assert account_balance(l2, chanid_3) == 0 + + # Verify we recorded all the movements we expect + check_coin_moves(l2, chanid_1, l1_l2_mvts) + check_coin_moves(l2, chanid_3, l2_l3_mvts) + check_coin_moves(l2, 'wallet', l2_wallet_mvts) diff --git a/tests/utils.py b/tests/utils.py index 80daf2c87..5f377afa8 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -27,3 +27,37 @@ def expected_channel_features(): return '80000000000000000000000000' else: return '' + + +def check_coin_moves(n, account_id, expected_moves): + moves = n.rpc.call('listcoinmoves_plugin')['coin_moves'] + node_id = n.info['id'] + acct_moves = [m for m in moves if m['account_id'] == account_id] + assert len(acct_moves) == len(expected_moves) + for mv, exp in list(zip(acct_moves, expected_moves)): + assert mv['version'] == 1 + assert mv['node_id'] == node_id + assert mv['type'] == exp['type'] + assert mv['credit'] == "{}msat".format(exp['credit']) + assert mv['debit'] == "{}msat".format(exp['debit']) + assert mv['tag'] == exp['tag'] + assert mv['timestamp'] > 0 + assert mv['unit_of_account'] == 'btc' + # chain moves should have blockheights + if mv['type'] == 'chain_mvt': + assert mv['blockheight'] is not None + + +def account_balance(n, account_id): + moves = n.rpc.call('listcoinmoves_plugin')['coin_moves'] + chan_moves = [m for m in moves if m['account_id'] == account_id] + assert len(chan_moves) > 0 + m_sum = 0 + for m in chan_moves: + m_sum += int(m['credit'][:-4]) + m_sum -= int(m['debit'][:-4]) + return m_sum + + +def first_channel_id(n1, n2): + return only_one(only_one(n1.rpc.listpeers(n2.info['id'])['peers'])['channels'])['channel_id']