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

openchannel: make openchannel hook chainable #3960

Merged
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
143 changes: 84 additions & 59 deletions lightningd/opening_control.c
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,8 @@ struct openchannel_hook_payload {
u16 max_accepted_htlcs;
u8 channel_flags;
u8 *shutdown_scriptpubkey;
const u8 *our_upfront_shutdown_script;
char *errmsg;
};

static void
Expand Down Expand Up @@ -803,13 +805,12 @@ static void openchannel_payload_remove_openingd(struct subd *openingd,
payload->openingd = NULL;
}

static void openchannel_hook_cb(struct openchannel_hook_payload *payload STEALS,
const char *buffer,
const jsmntok_t *toks)
static void
openchannel_hook_final(struct openchannel_hook_payload *payload STEALS)
{
struct subd *openingd = payload->openingd;
const u8 *our_upfront_shutdown_script;
const char *errmsg = NULL;
const u8 *our_upfront_shutdown_script = payload->our_upfront_shutdown_script;
const char *errmsg = payload->errmsg;

/* We want to free this, whatever happens. */
tal_steal(tmpctx, payload);
Expand All @@ -820,65 +821,87 @@ static void openchannel_hook_cb(struct openchannel_hook_payload *payload STEALS,

tal_del_destructor2(openingd, openchannel_payload_remove_openingd, payload);

/* If we had a hook, check what it says */
if (buffer) {
const jsmntok_t *t = json_get_member(buffer, toks, "result");
if (!t)
fatal("Plugin returned an invalid response to the"
" openchannel hook: %.*s",
toks[0].end - toks[0].start,
buffer + toks[0].start);

if (json_tok_streq(buffer, t, "reject")) {
t = json_get_member(buffer, toks, "error_message");
if (t)
errmsg = json_strdup(tmpctx, buffer, t);
else
errmsg = "";
log_debug(openingd->ld->log,
"openchannel_hook_cb says '%s'",
errmsg);
our_upfront_shutdown_script = NULL;
} else if (!json_tok_streq(buffer, t, "continue"))
fatal("Plugin returned an invalid result for the "
"openchannel hook: %.*s",
t->end - t->start, buffer + t->start);

/* Check for a 'close_to' address passed back */
if (!errmsg) {
t = json_get_member(buffer, toks, "close_to");
if (t) {
switch (json_to_address_scriptpubkey(tmpctx, chainparams,
buffer, t,
&our_upfront_shutdown_script)) {
case ADDRESS_PARSE_UNRECOGNIZED:
fatal("Plugin returned an invalid response to the"
" openchannel.close_to hook: %.*s",
t->end - t->start, buffer + t->start);
case ADDRESS_PARSE_WRONG_NETWORK:
fatal("Plugin returned invalid response to the"
" openchannel.close_to hook: address %s is"
" not on network %s",
tal_hex(NULL, our_upfront_shutdown_script),
chainparams->network_name);
case ADDRESS_PARSE_SUCCESS:
errmsg = NULL;
}
} else
our_upfront_shutdown_script = NULL;
}
} else
our_upfront_shutdown_script = NULL;

subd_send_msg(openingd,
take(towire_openingd_got_offer_reply(NULL, errmsg,
our_upfront_shutdown_script)));
}

REGISTER_SINGLE_PLUGIN_HOOK(openchannel,
openchannel_hook_cb,
openchannel_hook_serialize,
struct openchannel_hook_payload *);
static bool
openchannel_hook_deserialize(struct openchannel_hook_payload *payload,
const char *buffer,
const jsmntok_t *toks)
{
struct subd *openingd = payload->openingd;

/* already rejected by prior plugin hook in the chain */
if (payload->errmsg != NULL)
return true;

if (!toks || !buffer)
return true;

const jsmntok_t *t_result = json_get_member(buffer, toks, "result");
const jsmntok_t *t_errmsg = json_get_member(buffer, toks, "error_message");
const jsmntok_t *t_closeto = json_get_member(buffer, toks, "close_to");

if (!t_result)
fatal("Plugin returned an invalid response to the"
" openchannel hook: %.*s",
toks[0].end - toks[0].start, buffer + toks[0].start);

/* reject */
if (json_tok_streq(buffer, t_result, "reject")) {
payload->errmsg = "";
if (t_errmsg)
payload->errmsg = json_strdup(payload, buffer, t_errmsg);
log_debug(openingd->ld->log,
"openchannel_hook rejects and says '%s'",
payload->errmsg);
if (t_closeto)
fatal("Plugin rejected openchannel but also set close_to");
openchannel_hook_final(payload);
return false;
} else if (!json_tok_streq(buffer, t_result, "continue")) {
fatal("Plugin returned an invalid result for the "
"openchannel hook: %.*s",
t_result->end - t_result->start, buffer + t_result->start);
}

/* Check for a valid 'close_to' address passed back */
if (t_closeto) {
/* First plugin can set close_to. Log others. */
if (payload->our_upfront_shutdown_script != NULL) {
log_unusual(openingd->ld->log,
"openchannel_hook close_to address was"
" already set by other plugin. Ignoring!");
return true;
}
switch (json_to_address_scriptpubkey(tmpctx, chainparams,
buffer, t_closeto,
&payload->our_upfront_shutdown_script)) {
case ADDRESS_PARSE_UNRECOGNIZED:
fatal("Plugin returned an invalid response to"
" the openchannel.close_to hook: %.*s",
t_closeto->end - t_closeto->start,
buffer + t_closeto->start);
case ADDRESS_PARSE_WRONG_NETWORK:
fatal("Plugin returned invalid response to the"
" openchannel.close_to hook: address %s is"
" not on network %s",
tal_hex(NULL, payload->our_upfront_shutdown_script),
chainparams->network_name);
case ADDRESS_PARSE_SUCCESS:
break;
}
}
return true;
}

REGISTER_PLUGIN_HOOK(openchannel,
openchannel_hook_deserialize,
openchannel_hook_final,
openchannel_hook_serialize,
struct openchannel_hook_payload *);

static void opening_got_offer(struct subd *openingd,
const u8 *msg,
Expand All @@ -896,6 +919,8 @@ static void opening_got_offer(struct subd *openingd,

payload = tal(openingd, struct openchannel_hook_payload);
payload->openingd = openingd;
payload->our_upfront_shutdown_script = NULL;
payload->errmsg = NULL;
if (!fromwire_openingd_got_offer(payload, msg,
&payload->funding_satoshis,
&payload->push_msat,
Expand Down
35 changes: 0 additions & 35 deletions tests/plugins/accepter_close_to.py

This file was deleted.

19 changes: 19 additions & 0 deletions tests/plugins/openchannel_hook_accept.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env python3
"""Plugin to test openchannel_hook

Will simply accept any channel. Useful fot testing chained hook.
"""

from pyln.client import Plugin

plugin = Plugin()


@plugin.hook('openchannel')
def on_openchannel(openchannel, plugin, **kwargs):
msg = "accept on principle"
plugin.log(msg)
return {'result': 'continue'}


plugin.run()
51 changes: 51 additions & 0 deletions tests/plugins/openchannel_hook_accepter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Simple plugin to test the openchannel_hook's
'close_to' address functionality.

If the funding amount is:
- 100005sat: we reject correctly w/o close_to
- 100004sat: we reject invalid by setting a close_to
- 100003sat: we send back a valid address (regtest)
- 100002sat: we send back an empty address
- 100001sat: we send back an address for the wrong chain (mainnet)
- otherwise: we don't include the close_to
"""

from pyln.client import Plugin, Millisatoshi

plugin = Plugin()


@plugin.hook('openchannel')
def on_openchannel(openchannel, plugin, **kwargs):
# - 100005sat: we reject correctly w/o close_to
if Millisatoshi(openchannel['funding_satoshis']).to_satoshi() == 100005:
msg = "reject for a reason"
plugin.log(msg)
return {'result': 'reject', 'error_message': msg}

# - 100004sat: we reject invalid by setting a close_to
if Millisatoshi(openchannel['funding_satoshis']).to_satoshi() == 100004:
msg = "I am a broken plugin"
plugin.log(msg)
return {'result': 'reject', 'error_message': msg,
'close_to': "bcrt1q7gtnxmlaly9vklvmfj06amfdef3rtnrdazdsvw"}

# - 100003sat: we send back a valid address (regtest)
if Millisatoshi(openchannel['funding_satoshis']).to_satoshi() == 100003:
return {'result': 'continue', 'close_to': 'bcrt1q7gtnxmlaly9vklvmfj06amfdef3rtnrdazdsvw'}

# - 100002sat: we send back an empty address
if Millisatoshi(openchannel['funding_satoshis']).to_satoshi() == 100002:
return {'result': 'continue', 'close_to': ''}

# - 100001sat: we send back an address for the wrong chain (mainnet)
if Millisatoshi(openchannel['funding_satoshis']).to_satoshi() == 100001:
return {'result': 'continue', 'close_to': 'bc1qlq8srqnz64wgklmqvurv7qnr4rvtq2u96hhfg2'}

# - otherwise: accept and don't include the close_to
plugin.log("accept by design")
return {'result': 'continue'}


plugin.run()
20 changes: 20 additions & 0 deletions tests/plugins/openchannel_hook_reject.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env python3
"""Plugin to test openchannel_hook

Will simply reject any channel with message "reject on principle".
Useful fot testing chained hook.
"""

from pyln.client import Plugin

plugin = Plugin()


@plugin.hook('openchannel')
def on_openchannel(openchannel, plugin, **kwargs):
msg = "reject on principle"
plugin.log(msg)
return {'result': 'reject', 'error_message': msg}


plugin.run()
6 changes: 3 additions & 3 deletions tests/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -1050,13 +1050,13 @@ def test_funding_cancel_race(node_factory, bitcoind, executor):
def test_funding_close_upfront(node_factory, bitcoind):
l1 = node_factory.get_node()

opts = {'plugin': os.path.join(os.getcwd(), 'tests/plugins/accepter_close_to.py')}
opts = {'plugin': os.path.join(os.getcwd(), 'tests/plugins/openchannel_hook_accepter.py')}
l2 = node_factory.get_node(options=opts)

# The 'accepter_close_to' plugin uses the channel funding amount to determine
# whether or not to include a 'close_to' address
amt_normal = 100007 # continues without returning a close_to
amt_addr = 100001 # returns valid regtest address
amt_normal = 100000 # continues without returning a close_to
amt_addr = 100003 # returns valid regtest address

remote_valid_addr = 'bcrt1q7gtnxmlaly9vklvmfj06amfdef3rtnrdazdsvw'

Expand Down
Loading