Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: notification channel_state_changed #4020

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions contrib/pyln-testing/pyln/testing/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,15 @@ def get_channel_scid(self, other):
channel = peers[0]['channels'][0]
return channel['short_channel_id']

def get_channel_id(self, other):
"""Get the channel_id for the channel to the other node.
"""
peers = self.rpc.listpeers(other.info['id'])['peers']
if not peers or 'channels' not in peers[0]:
return None
channel = peers[0]['channels'][0]
return channel['channel_id']

def is_channel_active(self, chanid):
channels = self.rpc.listchannels(chanid)['channels']
active = [(c['short_channel_id'], c['channel_flags']) for c in channels if c['active']]
Expand Down
18 changes: 18 additions & 0 deletions doc/PLUGINS.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,24 @@ into a block.
}
```

### `channel_state_changed`

A notification for topic `channel_state_changed` is sent every time a channel
changes its state. The notification includes the peer and channel ids as well
as the old and the new channel states.

```json
{
"channel_state_changed": {
"peer_id": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251",
"channel_id": "a2d0851832f0e30a0cf778a826d72f077ca86b69f72677e0267f23f63a0599b4",
"short_channel_id" : "561820x1020x1",
"old_state": "CHANNELD_NORMAL",
"new_state": "CHANNELD_SHUTTING_DOWN"
}
}
```

### `connect`

A notification for topic `connect` is sent every time a new connection
Expand Down
14 changes: 14 additions & 0 deletions lightningd/channel.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <lightningd/jsonrpc.h>
#include <lightningd/lightningd.h>
#include <lightningd/log.h>
#include <lightningd/notification.h>
#include <lightningd/opening_control.h>
#include <lightningd/peer_control.h>
#include <lightningd/subd.h>
Expand Down Expand Up @@ -384,6 +385,8 @@ void channel_set_state(struct channel *channel,
enum channel_state old_state,
enum channel_state state)
{
struct channel_id cid;

log_info(channel->log, "State changed from %s to %s",
channel_state_name(channel), channel_state_str(state));
if (channel->state != old_state)
Expand All @@ -394,6 +397,17 @@ void channel_set_state(struct channel *channel,

/* TODO(cdecker) Selectively save updated fields to DB */
wallet_channel_save(channel->peer->ld->wallet, channel);

/* plugin notification channel_state_changed */
if (state != old_state) { /* see issue #4029 */
derive_channel_id(&cid, &channel->funding_txid, channel->funding_outnum);
notify_channel_state_changed(channel->peer->ld,
&channel->peer->id,
&cid,
channel->scid,
old_state,
state);
}
}

void channel_fail_permanent(struct channel *channel, const char *fmt, ...)
Expand Down
45 changes: 45 additions & 0 deletions lightningd/notification.c
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,51 @@ void notify_channel_opened(struct lightningd *ld, struct node_id *node_id,
plugins_notify(ld->plugins, take(n));
}

static void channel_state_changed_notification_serialize(struct json_stream *stream,
struct node_id *peer_id,
struct channel_id *cid,
struct short_channel_id *scid,
enum channel_state old_state,
enum channel_state new_state)
{
json_object_start(stream, "channel_state_changed");
json_add_node_id(stream, "peer_id", peer_id);
json_add_string(stream, "channel_id",
type_to_string(tmpctx, struct channel_id, cid));
if (scid)
json_add_short_channel_id(stream, "short_channel_id", scid);
else
json_add_null(stream, "short_channel_id");
json_add_string(stream, "old_state", channel_state_str(old_state));
json_add_string(stream, "new_state", channel_state_str(new_state));
json_object_end(stream);
}


REGISTER_NOTIFICATION(channel_state_changed,
channel_state_changed_notification_serialize)

void notify_channel_state_changed(struct lightningd *ld,
struct node_id *peer_id,
struct channel_id *cid,
struct short_channel_id *scid,
enum channel_state old_state,
enum channel_state new_state)
{
void (*serialize)(struct json_stream *,
struct node_id *,
struct channel_id *,
struct short_channel_id *,
enum channel_state,
enum channel_state) = channel_state_changed_notification_gen.serialize;

struct jsonrpc_notification *n
= jsonrpc_notification_start(NULL, channel_state_changed_notification_gen.topic);
serialize(n->stream, peer_id, cid, scid, old_state, new_state);
jsonrpc_notification_end(n);
plugins_notify(ld->plugins, take(n));
}

static void forward_event_notification_serialize(struct json_stream *stream,
const struct htlc_in *in,
const struct short_channel_id *scid_out,
Expand Down
9 changes: 9 additions & 0 deletions lightningd/notification.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
#include <ccan/json_escape/json_escape.h>
#include <ccan/time/time.h>
#include <common/amount.h>
#include <common/channel_id.h>
#include <common/coin_mvt.h>
#include <common/errcode.h>
#include <common/node_id.h>
#include <lightningd/channel_state.h>
#include <lightningd/htlc_end.h>
#include <lightningd/jsonrpc.h>
#include <lightningd/lightningd.h>
Expand Down Expand Up @@ -55,6 +57,13 @@ void notify_channel_opened(struct lightningd *ld, struct node_id *node_id,
struct amount_sat *funding_sat, struct bitcoin_txid *funding_txid,
bool *funding_locked);

void notify_channel_state_changed(struct lightningd *ld,
struct node_id *peer_id,
struct channel_id *cid,
struct short_channel_id *scid,
enum channel_state old_state,
enum channel_state new_state);

void notify_forward_event(struct lightningd *ld,
const struct htlc_in *in,
/* May be NULL if we don't know. */
Expand Down
7 changes: 6 additions & 1 deletion tests/plugins/misc_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@ def init(plugin, options, configuration):


@plugin.subscribe("channel_opened")
def channel_opened(plugin, channel_opened):
def channel_opened(plugin, channel_opened, **kwargs):
plugin.log("A channel was opened to us by {}, with an amount"
" of {} and the following funding transaction id: {}"
.format(channel_opened["id"], channel_opened["amount"],
channel_opened["funding_txid"]))


@plugin.subscribe("channel_state_changed")
def channel_state_changed(plugin, channel_state_changed, **kwargs):
plugin.log("channel_state_changed {}".format(channel_state_changed))


plugin.run()
117 changes: 117 additions & 0 deletions tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
check_coin_moves, first_channel_id, check_coin_moves_idx, EXPERIMENTAL_FEATURES
)

import ast
import json
import os
import pytest
Expand Down Expand Up @@ -631,6 +632,122 @@ def test_openchannel_hook_chaining(node_factory, bitcoind):
assert l2.daemon.wait_for_log(hook_msg + "reject on principle")


def test_channel_state_changed(node_factory, bitcoind):
opts = [{}, {"plugin": os.path.join(os.getcwd(), "tests/plugins/misc_notifications.py")}]
l1, l2 = node_factory.line_graph(2, opts=opts)

peer_id = l1.rpc.getinfo()["id"]
cid = l1.get_channel_id(l2)
scid = l1.get_channel_scid(l2)

msg = l2.daemon.wait_for_log("channel_state_changed.*new_state.*")
event = ast.literal_eval(re.findall(".*({.*}).*", msg)[0])
assert(event['peer_id'] == peer_id)
assert(event['channel_id'] == cid)
assert(event['short_channel_id'] == scid)
assert(event['old_state'] == "CHANNELD_AWAITING_LOCKIN")
assert(event['new_state'] == "CHANNELD_NORMAL")

# close channel and look for stateful events
l1.rpc.close(scid)
msg = l2.daemon.wait_for_log("channel_state_changed.*new_state.*")
event = ast.literal_eval(re.findall(".*({.*}).*", msg)[0])
assert(event['peer_id'] == peer_id)
assert(event['channel_id'] == cid)
assert(event['short_channel_id'] == scid)
assert(event['old_state'] == "CHANNELD_NORMAL")
assert(event['new_state'] == "CHANNELD_SHUTTING_DOWN")

msg = l2.daemon.wait_for_log("channel_state_changed.*new_state.*")
event = ast.literal_eval(re.findall(".*({.*}).*", msg)[0])
assert(event['peer_id'] == peer_id)
assert(event['channel_id'] == cid)
assert(event['short_channel_id'] == scid)
assert(event['old_state'] == "CHANNELD_SHUTTING_DOWN")
assert(event['new_state'] == "CLOSINGD_SIGEXCHANGE")

msg = l2.daemon.wait_for_log("channel_state_changed.*new_state.*")
event = ast.literal_eval(re.findall(".*({.*}).*", msg)[0])
assert(event['peer_id'] == peer_id)
assert(event['channel_id'] == cid)
assert(event['short_channel_id'] == scid)
assert(event['old_state'] == "CLOSINGD_SIGEXCHANGE")
assert(event['new_state'] == "CLOSINGD_COMPLETE")

bitcoind.generate_block(100) # so it gets settled

msg = l2.daemon.wait_for_log("channel_state_changed.*new_state.*")
event = ast.literal_eval(re.findall(".*({.*}).*", msg)[0])
assert(event['peer_id'] == peer_id)
assert(event['channel_id'] == cid)
assert(event['short_channel_id'] == scid)
assert(event['old_state'] == "CLOSINGD_COMPLETE")
assert(event['new_state'] == "FUNDING_SPEND_SEEN")

msg = l2.daemon.wait_for_log("channel_state_changed.*new_state.*")
event = ast.literal_eval(re.findall(".*({.*}).*", msg)[0])
assert(event['peer_id'] == peer_id)
assert(event['channel_id'] == cid)
assert(event['short_channel_id'] == scid)
assert(event['old_state'] == "FUNDING_SPEND_SEEN")
assert(event['new_state'] == "ONCHAIN")


def test_channel_state_changed_unilateral(node_factory, bitcoind):
opts = [{}, {"plugin": os.path.join(os.getcwd(), "tests/plugins/misc_notifications.py")}]
l1, l2 = node_factory.line_graph(2, opts=opts)

peer_id = l1.rpc.getinfo()["id"]
cid = l1.get_channel_id(l2)
scid = l1.get_channel_scid(l2)

msg = l2.daemon.wait_for_log("channel_state_changed.*new_state.*")
event = ast.literal_eval(re.findall(".*({.*}).*", msg)[0])
assert(event['peer_id'] == peer_id)
assert(event['channel_id'] == cid)
assert(event['short_channel_id'] == scid)
assert(event['old_state'] == "CHANNELD_AWAITING_LOCKIN")
assert(event['new_state'] == "CHANNELD_NORMAL")

# close channel unilaterally and look for stateful events
l1.rpc.stop()
wait_for(lambda: not only_one(l2.rpc.listpeers()['peers'])['connected'])
l2.rpc.close(scid, 1) # 1sec timeout
msg = l2.daemon.wait_for_log("channel_state_changed.*new_state.*")
event = ast.literal_eval(re.findall(".*({.*}).*", msg)[0])
assert(event['peer_id'] == peer_id)
assert(event['channel_id'] == cid)
assert(event['short_channel_id'] == scid)
assert(event['old_state'] == "CHANNELD_NORMAL")
assert(event['new_state'] == "CHANNELD_SHUTTING_DOWN")

msg = l2.daemon.wait_for_log("channel_state_changed.*new_state.*")
event = ast.literal_eval(re.findall(".*({.*}).*", msg)[0])
assert(event['peer_id'] == peer_id)
assert(event['channel_id'] == cid)
assert(event['short_channel_id'] == scid)
assert(event['old_state'] == "CHANNELD_SHUTTING_DOWN")
assert(event['new_state'] == "AWAITING_UNILATERAL")

bitcoind.generate_block(100) # so it gets settled

msg = l2.daemon.wait_for_log("channel_state_changed.*new_state.*")
event = ast.literal_eval(re.findall(".*({.*}).*", msg)[0])
assert(event['peer_id'] == peer_id)
assert(event['channel_id'] == cid)
assert(event['short_channel_id'] == scid)
assert(event['old_state'] == "AWAITING_UNILATERAL")
assert(event['new_state'] == "FUNDING_SPEND_SEEN")

msg = l2.daemon.wait_for_log("channel_state_changed.*new_state.*")
event = ast.literal_eval(re.findall(".*({.*}).*", msg)[0])
assert(event['peer_id'] == peer_id)
assert(event['channel_id'] == cid)
assert(event['short_channel_id'] == scid)
assert(event['old_state'] == "FUNDING_SPEND_SEEN")
assert(event['new_state'] == "ONCHAIN")


@unittest.skipIf(not DEVELOPER, "without DEVELOPER=1, gossip v slow")
def test_htlc_accepted_hook_fail(node_factory):
"""Send payments from l1 to l2, but l2 just declines everything.
Expand Down
2 changes: 1 addition & 1 deletion wallet/db_postgres_sqlgen.c

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion wallet/db_sqlite3_sqlgen.c

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions wallet/statements_gettextgen.po

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions wallet/test/run-wallet.c
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,14 @@ void notify_chain_mvt(struct lightningd *ld UNNEEDED, const struct chain_coin_mv
/* Generated stub for notify_channel_mvt */
void notify_channel_mvt(struct lightningd *ld UNNEEDED, const struct channel_coin_mvt *mvt UNNEEDED)
{ fprintf(stderr, "notify_channel_mvt called!\n"); abort(); }
/* Generated stub for notify_channel_state_changed */
void notify_channel_state_changed(struct lightningd *ld UNNEEDED,
struct node_id *peer_id UNNEEDED,
struct channel_id *cid UNNEEDED,
struct short_channel_id *scid UNNEEDED,
enum channel_state old_state UNNEEDED,
enum channel_state new_state UNNEEDED)
{ fprintf(stderr, "notify_channel_state_changed called!\n"); abort(); }
/* Generated stub for notify_connect */
void notify_connect(struct lightningd *ld UNNEEDED, struct node_id *nodeid UNNEEDED,
struct wireaddr_internal *addr UNNEEDED)
Expand Down