From 7c2a74684eb40ad1e751a90337565a5c357846ba Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 19 Aug 2025 10:30:45 +0930 Subject: [PATCH] lightningd: add listchainmoves and listchannelmoves commands. This is where all the previous work pays off: we can access the coinmoves in the db. Changelog-Added: JSON-RPC: `listchainmoves` and `listchannelmoves` commands to access the audit log of coin movements. Signed-off-by: Rusty Russell --- contrib/msggen/msggen/schema.json | 335 ++++++++++++++++++++++++++++++ doc/Makefile | 2 + doc/index.rst | 2 + doc/schemas/listchainmoves.json | 193 +++++++++++++++++ doc/schemas/listchannelmoves.json | 142 +++++++++++++ lightningd/coin_mvts.c | 107 ++++++++++ tests/test_coinmoves.py | 315 ++++++++++++++++++++++++++++ tests/utils.py | 1 + 8 files changed, 1097 insertions(+) create mode 100644 doc/schemas/listchainmoves.json create mode 100644 doc/schemas/listchannelmoves.json create mode 100644 tests/test_coinmoves.py diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index a7e99dac5..623265b33 100644 --- a/contrib/msggen/msggen/schema.json +++ b/contrib/msggen/msggen/schema.json @@ -16550,6 +16550,341 @@ } ] }, + "listchainmoves.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "rpc": "listchainmoves", + "title": "Command to get the audit list of all onchain coin movements.", + "added": "v25.09", + "description": [ + "The **listchainmoves** command returns the confirmed balance changes onchain over time." + ], + "categories": [ + "readonly" + ], + "request": { + "required": [], + "additionalProperties": false, + "properties": { + "index": { + "type": "string", + "enum": [ + "created" + ], + "description": [ + "How to interpret `start` and `limit`" + ] + }, + "start": { + "type": "u64", + "description": [ + "If `index` is specified, `start` may be specified to start from that value, which is generally returned from lightning-wait(7)." + ] + }, + "limit": { + "type": "u32", + "description": [ + "If `index` is specified, `limit` can be used to specify the maximum number of entries to return." + ] + } + }, + "dependentUpon": { + "index": [ + "start", + "limit" + ] + } + }, + "response": { + "required": [ + "chainmoves" + ], + "additionalProperties": false, + "properties": { + "chainmoves": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "account_id", + "primary_tag", + "extra_tags", + "credit_msat", + "debit_msat", + "timestamp", + "utxo", + "output_msat", + "blockheight" + ], + "properties": { + "account_id": { + "type": "string", + "description": [ + "This is either the channel_id corresponding to the channel involved, or a string such as `wallet` for the internal wallet, or `external` for some other external source." + ] + }, + "credit_msat": { + "type": "msat", + "description": [ + "Amount credited (one of this or debit_msat is zero)" + ] + }, + "debit_msat": { + "type": "msat", + "description": [ + "Amount debited (one of this or credit_msat is zero)" + ] + }, + "timestamp": { + "type": "u64", + "description": [ + "Time of this event in seconds since January 1 1970 UTC" + ] + }, + "primary_tag": { + "type": "string", + "enum": [ + "deposit", + "withdrawal", + "penalty", + "channel_open", + "channel_close", + "delayed_to_us", + "htlc_tx", + "htlc_timeout", + "htlc_fulfill", + "to_wallet", + "anchor", + "to_them", + "penalized", + "stolen", + "to_miner" + ], + "description": [ + "A set of one or more tags defining the nature of the change" + ] + }, + "extra_tags": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "ignored", + "opener", + "leased", + "stealable", + "splice" + ] + }, + "description": [ + "A set of additional tags expanding on the details" + ] + }, + "peer_id": { + "type": "pubkey", + "description": [ + "The lightning peer associated with this onchain event" + ] + }, + "originating_account": { + "type": "string", + "description": [ + "This is either a channel_id corresponding to the source channel involved, or a string such as `wallet` for the internal wallet." + ] + }, + "spending_txid": { + "type": "txid", + "description": [ + "The transaction ID which did the spending." + ] + }, + "utxo": { + "type": "outpoint", + "description": [ + "The txid and outpoint number spent for this balance change." + ] + }, + "payment_hash": { + "type": "hash", + "description": [ + "The payment hash associated with this balance change." + ] + }, + "output_msat": { + "type": "msat", + "description": [ + "The output amount (always a whole number of sats). Note that in some cases (e.g. channel opens), not all these belong to us." + ] + }, + "output_count": { + "type": "u32", + "description": [ + "The number of outputs in the `txid` (so you can tell once you've seen events for all of them)." + ] + }, + "blockheight": { + "type": "u32", + "description": [ + "The block number where `txid` appeared (alternately, where `utxo` was spent)." + ] + } + } + } + } + } + }, + "errors": [ + "On failure, one of the following error codes may be returned:", + "", + "- -32602: Error in given parameters." + ], + "resources": [ + "Main web site: " + ] + }, + "listchannelmoves.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "rpc": "listchannelmoves", + "title": "Command to get the audit list of all channel coin movements.", + "added": "v25.09", + "description": [ + "The **listchannelmoves** command returns the confirmed balance changes within lightning channels over time." + ], + "categories": [ + "readonly" + ], + "request": { + "required": [], + "additionalProperties": false, + "properties": { + "index": { + "type": "string", + "enum": [ + "created" + ], + "description": [ + "How to interpret `start` and `limit`" + ] + }, + "start": { + "type": "u64", + "description": [ + "If `index` is specified, `start` may be specified to start from that value, which is generally returned from lightning-wait(7)." + ] + }, + "limit": { + "type": "u32", + "description": [ + "If `index` is specified, `limit` can be used to specify the maximum number of entries to return." + ] + } + }, + "dependentUpon": { + "index": [ + "start", + "limit" + ] + } + }, + "response": { + "required": [ + "channelmoves" + ], + "additionalProperties": false, + "properties": { + "channelmoves": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "account_id", + "primary_tag", + "credit_msat", + "debit_msat", + "timestamp", + "fees_msat" + ], + "properties": { + "account_id": { + "type": "string", + "description": [ + "The channel_id corresponding to the channel involved." + ] + }, + "credit_msat": { + "type": "msat", + "description": [ + "Amount credited (one of this or debit_msat is zero)" + ] + }, + "debit_msat": { + "type": "msat", + "description": [ + "Amount debited (one of this or credit_msat is zero)" + ] + }, + "timestamp": { + "type": "u64", + "description": [ + "Time of this event in seconds since January 1 1970 UTC" + ] + }, + "primary_tag": { + "type": "string", + "enum": [ + "invoice", + "routed", + "pushed", + "lease_fee", + "channel_proposed", + "penalty_adj", + "journal" + ], + "description": [ + "A set of one or more tags defining the nature of the change" + ] + }, + "payment_hash": { + "type": "hash", + "description": [ + "The hash associated with this payment (not present for leases or push funding)" + ] + }, + "part_id": { + "type": "u64", + "description": [ + "The part_id for the payment (the `payment_hash`, `group_id`, `part_id` tuple will be unique)" + ] + }, + "group_id": { + "type": "u64", + "description": [ + "The group_id for the payment (the `payment_hash`, `group_id`, `part_id` tuple will be unique)" + ] + }, + "fees_msat": { + "type": "msat", + "description": [ + "The fees paid for this payment" + ] + } + } + } + } + } + }, + "errors": [ + "On failure, one of the following error codes may be returned:", + "", + "- -32602: Error in given parameters." + ], + "resources": [ + "Main web site: " + ] + }, "listchannels.json": { "$schema": "../rpc-schema-draft.json", "type": "object", diff --git a/doc/Makefile b/doc/Makefile index 3bb9d54df..7c8acd36e 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -77,6 +77,8 @@ MARKDOWNPAGES := doc/addgossip.7 \ doc/invoicerequest.7 \ doc/keysend.7 \ doc/listaddresses.7 \ + doc/listchainmoves.7 \ + doc/listchannelmoves.7 \ doc/listchannels.7 \ doc/listclosedchannels.7 \ doc/listconfigs.7 \ diff --git a/doc/index.rst b/doc/index.rst index 760914c21..a471fc38f 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -91,6 +91,8 @@ Core Lightning Documentation lightningd-config lightningd-rpc listaddresses + listchainmoves + listchannelmoves listchannels listclosedchannels listconfigs diff --git a/doc/schemas/listchainmoves.json b/doc/schemas/listchainmoves.json new file mode 100644 index 000000000..b8ec55522 --- /dev/null +++ b/doc/schemas/listchainmoves.json @@ -0,0 +1,193 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "rpc": "listchainmoves", + "title": "Command to get the audit list of all onchain coin movements.", + "added": "v25.09", + "description": [ + "The **listchainmoves** command returns the confirmed balance changes onchain over time." + ], + "categories": [ + "readonly" + ], + "request": { + "required": [], + "additionalProperties": false, + "properties": { + "index": { + "type": "string", + "enum": [ + "created" + ], + "description": [ + "How to interpret `start` and `limit`" + ] + }, + "start": { + "type": "u64", + "description": [ + "If `index` is specified, `start` may be specified to start from that value, which is generally returned from lightning-wait(7)." + ] + }, + "limit": { + "type": "u32", + "description": [ + "If `index` is specified, `limit` can be used to specify the maximum number of entries to return." + ] + } + }, + "dependentUpon": { + "index": [ + "start", + "limit" + ] + } + }, + "response": { + "required": [ + "chainmoves" + ], + "additionalProperties": false, + "properties": { + "chainmoves": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "account_id", + "primary_tag", + "extra_tags", + "credit_msat", + "debit_msat", + "timestamp", + "utxo", + "output_msat", + "blockheight" + ], + "properties": { + "account_id": { + "type": "string", + "description": [ + "This is either the channel_id corresponding to the channel involved, or a string such as `wallet` for the internal wallet, or `external` for some other external source." + ] + }, + "credit_msat": { + "type": "msat", + "description": [ + "Amount credited (one of this or debit_msat is zero)" + ] + }, + "debit_msat": { + "type": "msat", + "description": [ + "Amount debited (one of this or credit_msat is zero)" + ] + }, + "timestamp": { + "type": "u64", + "description": [ + "Time of this event in seconds since January 1 1970 UTC" + ] + }, + "primary_tag": { + "type": "string", + "enum": [ + "deposit", + "withdrawal", + "penalty", + "channel_open", + "channel_close", + "delayed_to_us", + "htlc_tx", + "htlc_timeout", + "htlc_fulfill", + "to_wallet", + "anchor", + "to_them", + "penalized", + "stolen", + "to_miner" + ], + "description": [ + "A set of one or more tags defining the nature of the change" + ] + }, + "extra_tags": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "ignored", + "opener", + "leased", + "stealable", + "splice" + ] + }, + "description": [ + "A set of additional tags expanding on the details" + ] + }, + "peer_id": { + "type": "pubkey", + "description": [ + "The lightning peer associated with this onchain event" + ] + }, + "originating_account": { + "type": "string", + "description": [ + "This is either a channel_id corresponding to the source channel involved, or a string such as `wallet` for the internal wallet." + ] + }, + "spending_txid": { + "type": "txid", + "description": [ + "The transaction ID which did the spending." + ] + }, + "utxo": { + "type": "outpoint", + "description": [ + "The txid and outpoint number spent for this balance change." + ] + }, + "payment_hash": { + "type": "hash", + "description": [ + "The payment hash associated with this balance change." + ] + }, + "output_msat": { + "type": "msat", + "description": [ + "The output amount (always a whole number of sats). Note that in some cases (e.g. channel opens), not all these belong to us." + ] + }, + "output_count": { + "type": "u32", + "description": [ + "The number of outputs in the `txid` (so you can tell once you've seen events for all of them)." + ] + }, + "blockheight": { + "type": "u32", + "description": [ + "The block number where `txid` appeared (alternately, where `utxo` was spent)." + ] + } + } + } + } + } + }, + "errors": [ + "On failure, one of the following error codes may be returned:", + "", + "- -32602: Error in given parameters." + ], + "resources": [ + "Main web site: " + ] +} diff --git a/doc/schemas/listchannelmoves.json b/doc/schemas/listchannelmoves.json new file mode 100644 index 000000000..fead46344 --- /dev/null +++ b/doc/schemas/listchannelmoves.json @@ -0,0 +1,142 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "rpc": "listchannelmoves", + "title": "Command to get the audit list of all channel coin movements.", + "added": "v25.09", + "description": [ + "The **listchannelmoves** command returns the confirmed balance changes within lightning channels over time." + ], + "categories": [ + "readonly" + ], + "request": { + "required": [], + "additionalProperties": false, + "properties": { + "index": { + "type": "string", + "enum": [ + "created" + ], + "description": [ + "How to interpret `start` and `limit`" + ] + }, + "start": { + "type": "u64", + "description": [ + "If `index` is specified, `start` may be specified to start from that value, which is generally returned from lightning-wait(7)." + ] + }, + "limit": { + "type": "u32", + "description": [ + "If `index` is specified, `limit` can be used to specify the maximum number of entries to return." + ] + } + }, + "dependentUpon": { + "index": [ + "start", + "limit" + ] + } + }, + "response": { + "required": [ + "channelmoves" + ], + "additionalProperties": false, + "properties": { + "channelmoves": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "account_id", + "primary_tag", + "credit_msat", + "debit_msat", + "timestamp", + "fees_msat" + ], + "properties": { + "account_id": { + "type": "string", + "description": [ + "The channel_id corresponding to the channel involved." + ] + }, + "credit_msat": { + "type": "msat", + "description": [ + "Amount credited (one of this or debit_msat is zero)" + ] + }, + "debit_msat": { + "type": "msat", + "description": [ + "Amount debited (one of this or credit_msat is zero)" + ] + }, + "timestamp": { + "type": "u64", + "description": [ + "Time of this event in seconds since January 1 1970 UTC" + ] + }, + "primary_tag": { + "type": "string", + "enum": [ + "invoice", + "routed", + "pushed", + "lease_fee", + "channel_proposed", + "penalty_adj", + "journal" + ], + "description": [ + "A set of one or more tags defining the nature of the change" + ] + }, + "payment_hash": { + "type": "hash", + "description": [ + "The hash associated with this payment (not present for leases or push funding)" + ] + }, + "part_id": { + "type": "u64", + "description": [ + "The part_id for the payment (the `payment_hash`, `group_id`, `part_id` tuple will be unique)" + ] + }, + "group_id": { + "type": "u64", + "description": [ + "The group_id for the payment (the `payment_hash`, `group_id`, `part_id` tuple will be unique)" + ] + }, + "fees_msat": { + "type": "msat", + "description": [ + "The fees paid for this payment" + ] + } + } + } + } + } + }, + "errors": [ + "On failure, one of the following error codes may be returned:", + "", + "- -32602: Error in given parameters." + ], + "resources": [ + "Main web site: " + ] +} diff --git a/lightningd/coin_mvts.c b/lightningd/coin_mvts.c index e31ede115..1ef0a213b 100644 --- a/lightningd/coin_mvts.c +++ b/lightningd/coin_mvts.c @@ -1,5 +1,6 @@ #include "config.h" #include +#include #include #include #include @@ -250,3 +251,109 @@ void json_add_channel_mvt_fields(struct json_stream *stream, } json_add_amount_msat(stream, "fees_msat", chan_mvt->fees); } + +static struct command_result *json_listchainmoves(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNNEEDED, + const jsmntok_t *params) +{ + struct json_stream *response; + struct db_stmt *stmt; + enum wait_index *listindex; + u64 *liststart; + u32 *listlimit; + + if (!param(cmd, buffer, params, + p_opt("index", param_index, &listindex), + p_opt_def("start", param_u64, &liststart, 0), + p_opt("limit", param_u32, &listlimit), + NULL)) + return command_param_failed(); + + if (*liststart != 0 && !listindex) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Can only specify {start} with {index}"); + } + if (listlimit && !listindex) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Can only specify {limit} with {index}"); + } + if (listindex && *listindex != WAIT_INDEX_CREATED) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "index must be 'created', since chainmoves are never updated"); + } + response = json_stream_success(cmd); + json_array_start(response, "chainmoves"); + for (stmt = wallet_chain_moves_first(cmd->ld->wallet, *liststart, listlimit); + stmt; + stmt = wallet_chain_moves_next(cmd->ld->wallet, stmt)) { + struct chain_coin_mvt *chain_mvt; + u64 id; + chain_mvt = wallet_chain_move_extract(cmd, stmt, cmd->ld, &id); + json_object_start(response, NULL); + json_add_chain_mvt_fields(response, false, false, false, chain_mvt); + json_object_end(response); + } + json_array_end(response); + + return command_success(cmd, response); +} +static const struct json_command listchainmoves_command = { + "listchainmoves", + json_listchainmoves, +}; +AUTODATA(json_command, &listchainmoves_command); + +static struct command_result *json_listchannelmoves(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNNEEDED, + const jsmntok_t *params) +{ + struct json_stream *response; + struct db_stmt *stmt; + enum wait_index *listindex; + u64 *liststart; + u32 *listlimit; + + if (!param(cmd, buffer, params, + p_opt("index", param_index, &listindex), + p_opt_def("start", param_u64, &liststart, 0), + p_opt("limit", param_u32, &listlimit), + NULL)) + return command_param_failed(); + + if (*liststart != 0 && !listindex) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Can only specify {start} with {index}"); + } + if (listlimit && !listindex) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Can only specify {limit} with {index}"); + } + if (listindex && *listindex != WAIT_INDEX_CREATED) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "index must be 'created', since channelmoves are never updated"); + } + + response = json_stream_success(cmd); + json_array_start(response, "channelmoves"); + for (stmt = wallet_channel_moves_first(cmd->ld->wallet, *liststart, listlimit); + stmt; + stmt = wallet_channel_moves_next(cmd->ld->wallet, stmt)) { + struct channel_coin_mvt *chan_mvt; + u64 id; + chan_mvt = wallet_channel_move_extract(cmd, stmt, cmd->ld, &id); + json_object_start(response, NULL); + /* No deprecated tags[], no extra_tags field */ + json_add_channel_mvt_fields(response, false, chan_mvt, false); + json_object_end(response); + } + json_array_end(response); + + return command_success(cmd, response); +} +static const struct json_command listchannelmoves_command = { + "listchannelmoves", + json_listchannelmoves, +}; +AUTODATA(json_command, &listchannelmoves_command); diff --git a/tests/test_coinmoves.py b/tests/test_coinmoves.py new file mode 100644 index 000000000..a17f55464 --- /dev/null +++ b/tests/test_coinmoves.py @@ -0,0 +1,315 @@ +from fixtures import * # noqa: F401,F403 +from fixtures import TEST_NETWORK +from utils import ( + sync_blockheight, wait_for, only_one +) + +import unittest +import pytest +from pyln.testing.utils import EXPERIMENTAL_DUAL_FUND + + +def check_moves(moves, expected): + for m in moves: + del m['timestamp'] + assert moves == expected + + +def check_channel_moves(node, expected): + check_moves(node.rpc.listchannelmoves()['channelmoves'], expected) + + +def check_chain_moves(node, expected): + check_moves(node.rpc.listchainmoves()['chainmoves'], expected) + + +@pytest.mark.openchannel('v1') +@pytest.mark.openchannel('v2') +@unittest.skipIf(TEST_NETWORK != 'regtest', "Amounts are for regtest.") +def test_coinmoves(node_factory, bitcoind): + l1, l2, l3 = node_factory.get_nodes(3) + + # Empty + expected_channel1 = [] + expected_channel2 = [] + expected_chain1 = [] + expected_chain2 = [] + check_channel_moves(l1, expected_channel1) + check_channel_moves(l2, expected_channel2) + check_chain_moves(l1, expected_chain1) + check_chain_moves(l2, expected_chain2) + + # MVT_DEPOSIT + addr = l1.rpc.newaddr()['bech32'] + txid_deposit = bitcoind.rpc.sendtoaddress(addr, 200000000 / 10**8) + bitcoind.generate_block(1, wait_for_mempool=1) + sync_blockheight(bitcoind, [l1]) + vout_deposit = only_one([out['n'] for out in bitcoind.rpc.gettransaction(txid_deposit, False, True)['decoded']['vout'] if out['scriptPubKey']['address'] == addr]) + + expected_chain1 += [{'account_id': 'wallet', + 'blockheight': 102, + 'credit_msat': 200000000000, + 'debit_msat': 0, + 'output_msat': 200000000000, + 'primary_tag': 'deposit', + 'extra_tags': [], + 'utxo': f"{txid_deposit}:{vout_deposit}"}] + check_channel_moves(l1, expected_channel1) + check_channel_moves(l2, expected_channel2) + check_chain_moves(l1, expected_chain1) + check_chain_moves(l2, expected_chain2) + + # MVT_WITHDRAWAL + addr = l3.rpc.newaddr()['bech32'] + withdraw = l1.rpc.withdraw(addr, 100000000) + bitcoind.generate_block(1, wait_for_mempool=1) + sync_blockheight(bitcoind, [l1]) + vout_withdrawal = only_one([out['n'] for out in bitcoind.rpc.decoderawtransaction(withdraw['tx'])['vout'] if out['scriptPubKey']['address'] == addr]) + + expected_chain1 += [{'account_id': 'external', + 'blockheight': 0, + 'credit_msat': 100000000000, + 'debit_msat': 0, + 'output_msat': 100000000000, + 'originating_account': 'wallet', + 'primary_tag': 'deposit', + 'extra_tags': [], + 'utxo': f"{withdraw['txid']}:{vout_withdrawal}"}, + # Spend + {'account_id': 'wallet', + 'blockheight': 103, + 'credit_msat': 0, + 'debit_msat': 200000000000, + 'primary_tag': 'withdrawal', + 'output_msat': 200000000000, + 'extra_tags': [], + 'spending_txid': withdraw['txid'], + 'utxo': f"{txid_deposit}:{vout_deposit}"}, + # Change + {'account_id': 'wallet', + 'blockheight': 103, + 'credit_msat': 99995433000, + 'debit_msat': 0, + 'primary_tag': 'deposit', + 'extra_tags': [], + 'output_msat': 99995433000, + 'utxo': f"{withdraw['txid']}:{vout_withdrawal ^ 1}"}] + check_channel_moves(l1, expected_channel1) + check_channel_moves(l2, expected_channel2) + check_chain_moves(l1, expected_chain1) + check_chain_moves(l2, expected_chain2) + + # MVT_CHANNEL_OPEN + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + fundchannel = l1.rpc.fundchannel(l2.info['id'], 'all') + bitcoind.generate_block(1, wait_for_mempool=fundchannel['txid']) + wait_for(lambda: all([c['state'] == 'CHANNELD_NORMAL' for c in l1.rpc.listpeerchannels(l2.info['id'])['channels']])) + expected_chain1 += [{'account_id': 'wallet', + 'blockheight': 104, + 'credit_msat': 0, + 'debit_msat': 99995433000, + 'output_msat': 99995433000, + 'primary_tag': 'withdrawal', + 'extra_tags': [], + 'spending_txid': fundchannel['txid'], + 'utxo': f"{withdraw['txid']}:{vout_withdrawal ^ 1}"}, + {'account_id': 'wallet', + 'blockheight': 104, + 'credit_msat': 25000000, + 'debit_msat': 0, + 'output_msat': 25000000, + 'primary_tag': 'deposit', + 'extra_tags': [], + 'utxo': f"{fundchannel['txid']}:{fundchannel['outnum'] ^ 1}"}, + {'account_id': fundchannel['channel_id'], + 'blockheight': 104, + 'credit_msat': 99965813000, + 'debit_msat': 0, + 'output_msat': 99965813000, + 'peer_id': l2.info['id'], + 'primary_tag': 'channel_open', + 'extra_tags': ['opener'], + 'utxo': f"{fundchannel['txid']}:{fundchannel['outnum']}"}] + expected_chain2 += [{'account_id': fundchannel['channel_id'], + 'blockheight': 104, + 'credit_msat': 0, + 'debit_msat': 0, + 'output_msat': 99965813000, + 'peer_id': l1.info['id'], + 'primary_tag': 'channel_open', + 'extra_tags': [], + 'utxo': f"{fundchannel['txid']}:{fundchannel['outnum']}"}] + check_channel_moves(l1, expected_channel1) + check_channel_moves(l2, expected_channel2) + check_chain_moves(l1, expected_chain1) + check_chain_moves(l2, expected_chain2) + + # MVT_INVOICE + inv = l2.rpc.invoice('any', 'test_coinmoves', 'test_coinmoves') + l1.rpc.xpay(inv['bolt11'], '1000sat') + # Make sure it's fully settled. + wait_for(lambda: only_one(l1.rpc.listpeerchannels(l2.info['id'])['channels'])['htlcs'] == []) + # We cheat and extract group id. + group_id = l1.rpc.listchannelmoves()['channelmoves'][-1]['group_id'] + expected_channel1 += [{'account_id': fundchannel['channel_id'], + 'credit_msat': 0, + 'debit_msat': 1000000, + 'primary_tag': 'invoice', + 'fees_msat': 0, + 'payment_hash': inv['payment_hash'], + 'group_id': group_id, + 'part_id': 1}] + expected_channel2 += [{'account_id': fundchannel['channel_id'], + 'credit_msat': 1000000, + 'debit_msat': 0, + 'primary_tag': 'invoice', + 'fees_msat': 0, + 'payment_hash': inv['payment_hash']}] + check_channel_moves(l1, expected_channel1) + check_channel_moves(l2, expected_channel2) + check_chain_moves(l1, expected_chain1) + check_chain_moves(l2, expected_chain2) + + # MVT_PUSHED + l3.rpc.connect(l1.info['id'], 'localhost', l1.port) + if not EXPERIMENTAL_DUAL_FUND: + l3fundchannel = l3.rpc.fundchannel(l1.info['id'], 40000000, push_msat=100000) + else: + l3fundchannel = l3.rpc.fundchannel(l1.info['id'], 40000000) + bitcoind.generate_block(1, wait_for_mempool=1) + wait_for(lambda: all([c['state'] == 'CHANNELD_NORMAL' for c in l1.rpc.listpeerchannels(l3.info['id'])['channels']])) + expected_chain1 += [{'account_id': l3fundchannel['channel_id'], + 'blockheight': 105, + 'credit_msat': 0, + 'debit_msat': 0, + 'output_msat': 40000000000, + 'peer_id': l3.info['id'], + 'primary_tag': 'channel_open', + 'extra_tags': [], + 'utxo': f"{l3fundchannel['txid']}:{l3fundchannel['outnum']}"}] + if not EXPERIMENTAL_DUAL_FUND: + expected_channel1 += [{'account_id': l3fundchannel['channel_id'], + 'credit_msat': 100000, + 'debit_msat': 0, + 'fees_msat': 0, + 'primary_tag': 'pushed'}] + check_channel_moves(l1, expected_channel1) + check_channel_moves(l2, expected_channel2) + check_chain_moves(l1, expected_chain1) + check_chain_moves(l2, expected_chain2) + + # MVT_ROUTED + # Make sure l3 sees l2. + bitcoind.generate_block(5) + wait_for(lambda: len(l3.rpc.listchannels()['channels']) == 4) + inv = l2.rpc.invoice('any', 'test_coinmoves2', 'test_coinmoves2') + l3.rpc.xpay(inv['bolt11'], '10000000sat') + # Make sure it's fully settled. + wait_for(lambda: only_one(l3.rpc.listpeerchannels(l1.info['id'])['channels'])['htlcs'] == []) + expected_channel1 += [{'account_id': fundchannel['channel_id'], + 'credit_msat': 0, + 'debit_msat': 10000000000, + 'fees_msat': 100001, + 'payment_hash': inv['payment_hash'], + 'primary_tag': 'routed'}, + {'account_id': l3fundchannel['channel_id'], + 'credit_msat': 10000100001, + 'debit_msat': 0, + 'fees_msat': 100001, + 'payment_hash': inv['payment_hash'], + 'primary_tag': 'routed'}] + expected_channel2 += [{'account_id': fundchannel['channel_id'], + 'credit_msat': 10000000000, + 'debit_msat': 0, + 'fees_msat': 0, + 'payment_hash': inv['payment_hash'], + 'primary_tag': 'invoice'}] + check_channel_moves(l1, expected_channel1) + check_channel_moves(l2, expected_channel2) + check_chain_moves(l1, expected_chain1) + check_chain_moves(l2, expected_chain2) + + # MVT_CHANNEL_CLOSE + close = l1.rpc.close(fundchannel['channel_id']) + bitcoind.generate_block(1, wait_for_mempool=1) + sync_blockheight(bitcoind, [l1, l2]) + # Determining our own output is harder than you might think! + l1_addrs = [a['p2tr'] for a in l1.rpc.listaddresses()['addresses'] if 'p2tr' in a] + l1_vout_close = only_one([out['n'] for out in bitcoind.rpc.decoderawtransaction(only_one(close['txs']))['vout'] if out['scriptPubKey']['address'] in l1_addrs]) + expected_chain1 += [{'account_id': 'wallet', + 'blockheight': 111, + 'credit_msat': 89961918000, + 'debit_msat': 0, + 'extra_tags': [], + 'output_msat': 89961918000, + 'primary_tag': 'deposit', + 'utxo': f"{only_one(close['txids'])}:{l1_vout_close}"}, + {'account_id': fundchannel['channel_id'], + 'blockheight': 111, + 'credit_msat': 0, + 'debit_msat': 89964813000, + 'extra_tags': [], + 'output_count': 2, + 'output_msat': 99965813000, + 'primary_tag': 'channel_close', + 'spending_txid': only_one(close['txids']), + 'utxo': f"{fundchannel['txid']}:{fundchannel['outnum']}"}, + {'account_id': 'external', + 'blockheight': 111, + 'credit_msat': 10001000000, + 'debit_msat': 0, + 'extra_tags': [], + 'originating_account': fundchannel['channel_id'], + 'output_msat': 10001000000, + 'primary_tag': 'to_them', + 'utxo': f"{only_one(close['txids'])}:{l1_vout_close ^ 1}"}] + expected_chain2 += [{'account_id': 'wallet', + 'blockheight': 111, + 'credit_msat': 10001000000, + 'debit_msat': 0, + 'extra_tags': [], + 'output_msat': 10001000000, + 'primary_tag': 'deposit', + 'utxo': f"{only_one(close['txids'])}:{fundchannel['outnum']}"}, + {'account_id': fundchannel['channel_id'], + 'blockheight': 111, + 'credit_msat': 0, + 'debit_msat': 10001000000, + 'extra_tags': [], + 'output_count': 2, + 'output_msat': 99965813000, + 'primary_tag': 'channel_close', + 'spending_txid': only_one(close['txids']), + 'utxo': f"{fundchannel['txid']}:{fundchannel['outnum']}"}, + {'account_id': 'external', + 'blockheight': 111, + 'credit_msat': 89961918000, + 'debit_msat': 0, + 'extra_tags': [], + 'originating_account': fundchannel['channel_id'], + 'output_msat': 89961918000, + 'primary_tag': 'to_them', + 'utxo': f"{only_one(close['txids'])}:{fundchannel['outnum'] ^ 1}"}] + check_channel_moves(l1, expected_channel1) + check_channel_moves(l2, expected_channel2) + check_chain_moves(l1, expected_chain1) + check_chain_moves(l2, expected_chain2) + + # FIXME: + # MVT_PENALTY, + # MVT_CHANNEL_TO_US, + # MVT_HTLC_TIMEOUT, + # MVT_HTLC_FULFILL, + # MVT_HTLC_TX, + # MVT_TO_WALLET, + # MVT_ANCHOR, + # MVT_TO_THEM, + # MVT_PENALIZED, + # MVT_STOLEN, + # MVT_TO_MINER, + # MVT_LEASE_FEE, + # MVT_CHANNEL_PROPOSED, + # Extra tags + # MVT_SPLICE, + # MVT_LEASED, + # MVT_STEALABLE, diff --git a/tests/utils.py b/tests/utils.py index ad4ff8332..fc3f18b18 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -140,6 +140,7 @@ def check_coin_moves(n, account_id, expected_moves, chainparams): # Stash moves for errors, if needed _acct_moves = acct_moves for mv in acct_moves: + # Generate tags as single array, which is how bookkeeper presents it mv['tags'] = [mv['primary_tag']] + mv['extra_tags'] print("{{'type': '{}', 'credit_msat': {}, 'debit_msat': {}, 'tags': '{}' , ['fees_msat'?: '{}']}}," .format(mv['type'],