diff --git a/doc/Makefile b/doc/Makefile index 976a891c8..3536ba1d2 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -52,6 +52,7 @@ MANPAGES := doc/lightning-cli.1 \ doc/lightning-invoicerequest.7 \ doc/lightning-keysend.7 \ doc/lightning-listchannels.7 \ + doc/lightning-listclosedchannels.7 \ doc/lightning-listdatastore.7 \ doc/lightning-listforwards.7 \ doc/lightning-listfunds.7 \ diff --git a/doc/index.rst b/doc/index.rst index 26b78a6f9..a6ba64e94 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -78,6 +78,7 @@ Core Lightning Documentation lightning-invoicerequest lightning-keysend lightning-listchannels + lightning-listclosedchannels lightning-listconfigs lightning-listdatastore lightning-listforwards diff --git a/doc/lightning-listclosedchannels.7.md b/doc/lightning-listclosedchannels.7.md new file mode 100644 index 000000000..4e193428d --- /dev/null +++ b/doc/lightning-listclosedchannels.7.md @@ -0,0 +1,79 @@ +lightning-listclosedchannels -- Get data on our closed historical channels +========================================================================== + +SYNOPSIS +-------- + +**listclosedchannels** \[*id*\] + +DESCRIPTION +----------- + +The **listclosedchannels** RPC command returns data on channels which +are otherwise forgotten (more than 100 blocks after they're completely +resolved onchain). + +If no *id* is supplied, then channel data on all historical channels are given. + +Supplying *id* will filter the results to only match channels to that peer. Note that prior to v23.05, old peers were forgotten. + +RETURN VALUE +------------ + +[comment]: # (GENERATE-FROM-SCHEMA-START) +On success, an object containing **closedchannels** is returned. It is an array of objects, where each object contains: + +- **channel\_id** (hash): The full channel\_id (funding txid Xored with output number) +- **opener** (string): Who initiated the channel (one of "local", "remote") +- **private** (boolean): if False, we will not announce this channel +- **total\_local\_commitments** (u64): Number of commitment transaction we made +- **total\_remote\_commitments** (u64): Number of commitment transaction they made +- **total\_htlcs\_sent** (u64): Number of HTLCs we ever sent +- **funding\_txid** (txid): ID of the funding transaction +- **funding\_outnum** (u32): The 0-based output number of the funding transaction which opens the channel +- **leased** (boolean): Whether this channel was leased from `opener` +- **total\_msat** (msat): total amount in the channel +- **final\_to\_us\_msat** (msat): Our balance in final commitment transaction +- **min\_to\_us\_msat** (msat): Least amount owed to us ever. If the peer were to succesfully steal from us, this is the amount we would still retain. +- **max\_to\_us\_msat** (msat): Most amount owed to us ever. If we were to successfully steal from the peer, this is the amount we could potentially get. +- **close\_cause** (string): What caused the channel to close (one of "unknown", "local", "user", "remote", "protocol", "onchain") +- **peer\_id** (pubkey, optional): Peer public key (can be missing with pre-v23.05 closes!) +- **short\_channel\_id** (short\_channel\_id, optional): The short\_channel\_id +- **alias** (object, optional): + - **local** (short\_channel\_id, optional): An alias assigned by this node to this channel, used for outgoing payments + - **remote** (short\_channel\_id, optional): An alias assigned by the remote node to this channel, usable in routehints and invoices +- **closer** (string, optional): Who initiated the channel close (only present if closing) (one of "local", "remote") +- **channel\_type** (object, optional): channel\_type as negotiated with peer: + - **bits** (array of u32s): Each bit set in this channel\_type: + - Bit number + - **names** (array of strings): Feature name for each bit set in this channel\_type: + - Name of feature bit (one of "static\_remotekey/even", "anchor\_outputs/even", "anchors\_zero\_fee\_htlc\_tx/even", "scid\_alias/even", "zeroconf/even") +- **funding\_fee\_paid\_msat** (msat, optional): How much we paid to lease the channel (iff `leased` is true and `opener` is local) +- **funding\_fee\_rcvd\_msat** (msat, optional): How much they paid to lease the channel (iff `leased` is true and `opener` is remote) +- **funding\_pushed\_msat** (msat, optional): How much `opener` pushed immediate (if non-zero) +- **last\_commitment\_txid** (hash, optional): The final commitment tx's txid (or mutual close, if we accepted it). Not present for some very old, small channels pre-0.7.0. +- **last\_commitment\_fee\_msat** (msat, optional): The fee on `last_commitment_txid` + +[comment]: # (GENERATE-FROM-SCHEMA-END) + +On error the returned object will contain `code` and `message` properties, +with `code` being one of the following: + +- -32602: If the given parameters are wrong. + +AUTHOR +------ + +Rusty Russell <>. + +SEE ALSO +-------- + +lightning-listpeerchannels(7) + +RESOURCES +--------- + +Main web site: Lightning + +[comment]: # ( SHA256STAMP:0c368cb41f46a2124e9b3f0b760494d1f4b9c3b248267f56b887fbf96f26e176) diff --git a/doc/schemas/listclosedchannels.request.json b/doc/schemas/listclosedchannels.request.json new file mode 100644 index 000000000..2726913be --- /dev/null +++ b/doc/schemas/listclosedchannels.request.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [], + "additionalProperties": false, + "added": "v23.05", + "properties": { + "id": { + "type": "pubkey", + "description": "If supplied, limits the channels to just the peer with the given ID, if it exists." + } + } +} diff --git a/doc/schemas/listclosedchannels.schema.json b/doc/schemas/listclosedchannels.schema.json new file mode 100644 index 000000000..7ee9ae5c4 --- /dev/null +++ b/doc/schemas/listclosedchannels.schema.json @@ -0,0 +1,188 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "added": "v23.05", + "required": [ + "closedchannels" + ], + "properties": { + "closedchannels": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true, + "required": [ + "channel_id", + "opener", + "private", + "total_msat", + "total_local_commitments", + "total_remote_commitments", + "total_htlcs_sent", + "funding_txid", + "funding_outnum", + "leased", + "final_to_us_msat", + "min_to_us_msat", + "max_to_us_msat", + "close_cause" + ], + "properties": { + "peer_id": { + "type": "pubkey", + "description": "Peer public key (can be missing with pre-v23.05 closes!)" + }, + "channel_id": { + "type": "hash", + "description": "The full channel_id (funding txid Xored with output number)" + }, + "short_channel_id": { + "type": "short_channel_id", + "description": "The short_channel_id" + }, + "alias": { + "type": "object", + "required": [], + "properties": { + "local": { + "type": "short_channel_id", + "description": "An alias assigned by this node to this channel, used for outgoing payments" + }, + "remote": { + "type": "short_channel_id", + "description": "An alias assigned by the remote node to this channel, usable in routehints and invoices" + } + } + }, + "opener": { + "type": "string", + "enum": [ + "local", + "remote" + ], + "description": "Who initiated the channel" + }, + "closer": { + "type": "string", + "enum": [ + "local", + "remote" + ], + "description": "Who initiated the channel close (only present if closing)" + }, + "private": { + "type": "boolean", + "description": "if False, we will not announce this channel" + }, + "channel_type": { + "type": "object", + "description": "channel_type as negotiated with peer", + "additionalProperties": false, + "required": [ + "bits", + "names" + ], + "properties": { + "bits": { + "type": "array", + "description": "Each bit set in this channel_type", + "items": { + "type": "u32", + "description": "Bit number" + } + }, + "names": { + "type": "array", + "description": "Feature name for each bit set in this channel_type", + "items": { + "type": "string", + "enum": [ + "static_remotekey/even", + "anchor_outputs/even", + "anchors_zero_fee_htlc_tx/even", + "scid_alias/even", + "zeroconf/even" + ], + "description": "Name of feature bit" + } + } + } + }, + "total_local_commitments": { + "type": "u64", + "description": "Number of commitment transaction we made" + }, + "total_remote_commitments": { + "type": "u64", + "description": "Number of commitment transaction they made" + }, + "total_htlcs_sent": { + "type": "u64", + "description": "Number of HTLCs we ever sent" + }, + "funding_txid": { + "type": "txid", + "description": "ID of the funding transaction" + }, + "funding_outnum": { + "type": "u32", + "description": "The 0-based output number of the funding transaction which opens the channel" + }, + "leased": { + "type": "boolean", + "description": "Whether this channel was leased from `opener`" + }, + "funding_fee_paid_msat": { + "type": "msat", + "description": "How much we paid to lease the channel (iff `leased` is true and `opener` is local)" + }, + "funding_fee_rcvd_msat": { + "type": "msat", + "description": "How much they paid to lease the channel (iff `leased` is true and `opener` is remote)" + }, + "funding_pushed_msat": { + "type": "msat", + "description": "How much `opener` pushed immediate (if non-zero)" + }, + "total_msat": { + "type": "msat", + "description": "total amount in the channel" + }, + "final_to_us_msat": { + "type": "msat", + "description": "Our balance in final commitment transaction" + }, + "min_to_us_msat": { + "type": "msat", + "description": "Least amount owed to us ever. If the peer were to succesfully steal from us, this is the amount we would still retain." + }, + "max_to_us_msat": { + "type": "msat", + "description": "Most amount owed to us ever. If we were to successfully steal from the peer, this is the amount we could potentially get." + }, + "last_commitment_txid": { + "type": "hash", + "description": "The final commitment tx's txid (or mutual close, if we accepted it). Not present for some very old, small channels pre-0.7.0." + }, + "last_commitment_fee_msat": { + "type": "msat", + "description": "The fee on `last_commitment_txid`" + }, + "close_cause": { + "type": "string", + "enum": [ + "unknown", + "local", + "user", + "remote", + "protocol", + "onchain" + ], + "description": "What caused the channel to close" + } + } + } + } + } +} diff --git a/lightningd/Makefile b/lightningd/Makefile index f277ec3b2..6cc220dcf 100644 --- a/lightningd/Makefile +++ b/lightningd/Makefile @@ -8,6 +8,7 @@ LIGHTNINGD_SRC := \ lightningd/closing_control.c \ lightningd/coin_mvts.c \ lightningd/dual_open_control.c \ + lightningd/closed_channel.c \ lightningd/connect_control.c \ lightningd/onion_message.c \ lightningd/feerate.c \ @@ -48,7 +49,6 @@ include wallet/Makefile LIGHTNINGD_HDRS := \ $(LIGHTNINGD_SRC:.c=.h) \ - lightningd/closed_channel.h \ lightningd/channel_state.h \ lightningd/channel_state_names_gen.h \ $(WALLET_HDRS) diff --git a/lightningd/closed_channel.c b/lightningd/closed_channel.c new file mode 100644 index 000000000..89998165d --- /dev/null +++ b/lightningd/closed_channel.c @@ -0,0 +1,119 @@ +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void json_add_closed_channel(struct json_stream *response, + const char *fieldname, + const struct closed_channel *channel) +{ + json_object_start(response, fieldname); + if (channel->peer_id) + json_add_node_id(response, "peer_id", channel->peer_id); + json_add_channel_id(response, "channel_id", &channel->cid); + if (channel->scid) + json_add_short_channel_id(response, "short_channel_id", + channel->scid); + if (channel->alias[LOCAL] || channel->alias[REMOTE]) { + json_object_start(response, "alias"); + if (channel->alias[LOCAL]) + json_add_short_channel_id(response, "local", + channel->alias[LOCAL]); + if (channel->alias[REMOTE]) + json_add_short_channel_id(response, "remote", + channel->alias[REMOTE]); + json_object_end(response); + } + json_add_string(response, "opener", + channel->opener == LOCAL ? "local" : "remote"); + if (channel->closer != NUM_SIDES) + json_add_string(response, "closer", channel->closer == LOCAL ? + "local" : "remote"); + + json_add_bool(response, "private", + !(channel->channel_flags & CHANNEL_FLAGS_ANNOUNCE_CHANNEL)); + + json_add_channel_type(response, "channel_type", channel->type); + json_add_u64(response, "total_local_commitments", + channel->next_index[LOCAL] - 1); + json_add_u64(response, "total_remote_commitments", + channel->next_index[REMOTE] - 1); + json_add_u64(response, "total_htlcs_sent", channel->next_htlc_id); + json_add_txid(response, "funding_txid", &channel->funding.txid); + json_add_num(response, "funding_outnum", channel->funding.n); + json_add_bool(response, "leased", channel->leased); + if (channel->leased) { + if (channel->opener == LOCAL) + json_add_amount_msat(response, "funding_fee_paid_msat", + channel->push); + else + json_add_amount_msat(response, "funding_fee_rcvd_msat", + channel->push); + } else if (!amount_msat_eq(channel->push, AMOUNT_MSAT(0))) + json_add_amount_msat(response, "funding_pushed_msat", + channel->push); + + json_add_amount_sat_msat(response, "total_msat", channel->funding_sats); + json_add_amount_msat(response, "final_to_us_msat", channel->our_msat); + json_add_amount_msat(response, "min_to_us_msat", + channel->msat_to_us_min); + json_add_amount_msat(response, "max_to_us_msat", + channel->msat_to_us_max); + if (channel->last_tx && !invalid_last_tx(channel->last_tx)) { + struct bitcoin_txid txid; + bitcoin_txid(channel->last_tx, &txid); + + json_add_txid(response, "last_commitment_txid", &txid); + json_add_amount_sat_msat(response, "last_commitment_fee_msat", + bitcoin_tx_compute_fee(channel->last_tx)); + } + json_add_string(response, "close_cause", + channel_change_state_reason_str(channel->state_change_cause)); + json_object_end(response); +} + +static struct command_result *json_listclosedchannels(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNNEEDED, + const jsmntok_t *params) +{ + struct node_id *peer_id; + struct json_stream *response; + struct closed_channel **chans; + + if (!param(cmd, buffer, params, + p_opt("id", param_node_id, &peer_id), + NULL)) + return command_param_failed(); + + response = json_stream_success(cmd); + json_array_start(response, "closedchannels"); + + chans = wallet_load_closed_channels(cmd, cmd->ld->wallet); + for (size_t i = 0; i < tal_count(chans); i++) { + if (peer_id) { + if (!chans[i]->peer_id) + continue; + if (!node_id_eq(chans[i]->peer_id, peer_id)) + continue; + } + json_add_closed_channel(response, NULL, chans[i]); + } + json_array_end(response); + + return command_success(cmd, response); +} + +static const struct json_command listclosedchannels_command = { + "listclosedchannels", + "network", + json_listclosedchannels, + "Show historical (dead) channels." +}; +AUTODATA(json_command, &listclosedchannels_command); diff --git a/lightningd/closed_channel.h b/lightningd/closed_channel.h index d507f5b84..44a2269fe 100644 --- a/lightningd/closed_channel.h +++ b/lightningd/closed_channel.h @@ -2,6 +2,10 @@ #ifndef LIGHTNING_LIGHTNINGD_CLOSED_CHANNEL_H #define LIGHTNING_LIGHTNINGD_CLOSED_CHANNEL_H #include "config.h" +#include +#include +#include +#include struct closed_channel { /* This is often deleted on older nodes! */ diff --git a/tests/test_closing.py b/tests/test_closing.py index 72da1d9cc..02876d608 100644 --- a/tests/test_closing.py +++ b/tests/test_closing.py @@ -95,14 +95,50 @@ def test_closing_simple(node_factory, bitcoind, chainparams): 'ONCHAIN:All outputs resolved: waiting 90 more blocks before forgetting channel' ]) + # Capture both side's image of channel before it's dead. + l1channel = only_one(l1.rpc.listpeerchannels(l2.info['id'])['channels']) + l2channel = only_one(l2.rpc.listpeerchannels(l1.info['id'])['channels']) + # Make sure both have forgotten about it bitcoind.generate_block(90) - wait_for(lambda: len(l1.rpc.listchannels()['channels']) == 0) - wait_for(lambda: len(l2.rpc.listchannels()['channels']) == 0) + wait_for(lambda: len(l1.rpc.listpeerchannels()['channels']) == 0) + wait_for(lambda: len(l2.rpc.listpeerchannels()['channels']) == 0) # The entry in the channels table should still be there assert l1.db_query("SELECT count(*) as c FROM channels;")[0]['c'] == 1 assert l2.db_query("SELECT count(*) as c FROM channels;")[0]['c'] == 1 + assert l1.db_query("SELECT count(*) as p FROM peers;")[0]['p'] == 1 + assert l2.db_query("SELECT count(*) as p FROM peers;")[0]['p'] == 1 + + # Test listclosedchannels is correct. + l1closedchannel = only_one(l1.rpc.listclosedchannels()['closedchannels']) + l2closedchannel = only_one(l2.rpc.listclosedchannels(l1.info['id'])['closedchannels']) + + # These fields do not appear in listpeerchannels! + l1_only_closed = {'total_local_commitments': 2, + 'total_remote_commitments': 2, + 'total_htlcs_sent': 1, + 'leased': False, + 'close_cause': 'user'} + l2_only_closed = {'total_local_commitments': 2, + 'total_remote_commitments': 2, + 'total_htlcs_sent': 0, + 'leased': False, + 'close_cause': 'remote'} + + # These fields have different names + renamed = {'last_commitment_txid': 'scratch_txid', + 'last_commitment_fee_msat': 'last_tx_fee_msat', + 'final_to_us_msat': 'to_us_msat'} + + for chan, closedchan, onlyclosed in (l1channel, l1closedchannel, l1_only_closed), (l2channel, l2closedchannel, l2_only_closed): + for k, v in closedchan.items(): + if k in renamed: + assert chan[renamed[k]] == v + elif k in onlyclosed: + assert closedchan[k] == onlyclosed[k] + else: + assert chan[k] == v assert account_balance(l1, channel_id) == 0 assert account_balance(l2, channel_id) == 0