lightningd: add channel_type to openchannel & openchannel2 hooks.
What changed, and why it matters
This commit adds a missing piece of information—the channel type—to two plugin hooks used when opening Lightning channels. It is a straightforward feature addition that lets plugins see what kind of channel is being offered. There is no indication it fixes a security bug or introduces a vulnerability.
No security action required. Treat as a normal feature/documentation update during review.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change exposes channel_type in the openchannel and openchannel2 hook payloads sent to plugins. It threads the existing channel_type/ctype field through the internal wire protocols (openingd_got_offer, dualopend_got_offer) and serializes it into the JSON hook payload. Documentation and tests are updated accordingly. No security-sensitive logic is modified.
Changed components
lightningd/opening_control.clightningd/dual_open_control.copeningd/openingd.copeningd/dualopend.copeningd/openingd_wire.csvopeningd/dualopend_wire.csvdoc/developers-guide/plugin-development/hooks.mdtests/plugins/openchannel_hook_accepter.pytests/test_plugin.pyInspect captured patch +35 / −8
diff --git a/doc/developers-guide/plugin-development/hooks.md b/doc/developers-guide/plugin-development/hooks.md
index 4dd75fe9..b8443387 100644
--- a/doc/developers-guide/plugin-development/hooks.md
+++ b/doc/developers-guide/plugin-development/hooks.md
@@ -245,6 +245,7 @@ This hook is called whenever a remote peer tries to fund a channel to us using t
"to_self_delay": 5,
"max_accepted_htlcs": 483,
"channel_flags": 1,
+ "channel_type": {'bits': [12, 22], 'names': ['static_remotekey/even', 'anchors/even']},
"locktime": 2453,
"channel_max_msat": 16777215000,
"requested_lease_msat": 100000000,
diff --git a/lightningd/dual_open_control.c b/lightningd/dual_open_control.c
index 8e106c1b..fe995d3d 100644
--- a/lightningd/dual_open_control.c
+++ b/lightningd/dual_open_control.c
@@ -274,6 +274,7 @@ struct openchannel2_payload {
u32 lease_blockheight_start;
u32 node_blockheight;
bool req_confirmed_ins_remote;
+ struct channel_type *channel_type;
struct amount_sat accepter_funding;
struct wally_psbt *psbt;
@@ -325,6 +326,7 @@ static void openchannel2_hook_serialize(struct openchannel2_payload *payload,
}
json_add_bool(stream, "require_confirmed_inputs",
payload->req_confirmed_ins_remote);
+ json_add_channel_type(stream, "channel_type", payload->channel_type);
json_object_end(stream);
}
@@ -2098,7 +2100,8 @@ static void accepter_got_offer(struct subd *dualopend,
&payload->shutdown_scriptpubkey,
&payload->requested_lease_amt,
&payload->lease_blockheight_start,
- &payload->req_confirmed_ins_remote)) {
+ &payload->req_confirmed_ins_remote,
+ &payload->channel_type)) {
channel_internal_error(channel, "Bad DUALOPEND_GOT_OFFER: %s",
tal_hex(tmpctx, msg));
return;
diff --git a/lightningd/opening_control.c b/lightningd/opening_control.c
index 4924923a..7c12a0ff 100644
--- a/lightningd/opening_control.c
+++ b/lightningd/opening_control.c
@@ -651,6 +651,7 @@ struct openchannel_hook_payload {
u8 channel_flags;
u8 *shutdown_scriptpubkey;
const u8 *our_upfront_shutdown_script;
+ struct channel_type *channel_type;
char *errmsg;
};
@@ -679,6 +680,7 @@ static void openchannel_hook_serialize(struct openchannel_hook_payload *payload,
if (tal_count(payload->shutdown_scriptpubkey) != 0)
json_add_hex_talarr(stream, "shutdown_scriptpubkey",
payload->shutdown_scriptpubkey);
+ json_add_channel_type(stream, "channel_type", payload->channel_type);
json_object_end(stream); /* .openchannel */
}
@@ -858,7 +860,8 @@ static void opening_got_offer(struct subd *openingd,
&payload->to_self_delay,
&payload->max_accepted_htlcs,
&payload->channel_flags,
- &payload->shutdown_scriptpubkey)) {
+ &payload->shutdown_scriptpubkey,
+ &payload->channel_type)) {
log_broken(openingd->log, "Malformed opening_got_offer %s",
tal_hex(tmpctx, msg));
tal_free(openingd);
diff --git a/openingd/dualopend.c b/openingd/dualopend.c
index 357ba939..d140d37b 100644
--- a/openingd/dualopend.c
+++ b/openingd/dualopend.c
@@ -2532,7 +2532,8 @@ static void accepter_start(struct state *state, const u8 *oc2_msg)
state->upfront_shutdown_script[REMOTE],
state->requested_lease,
tx_state->blockheight,
- state->require_confirmed_inputs[REMOTE]);
+ state->require_confirmed_inputs[REMOTE],
+ state->channel_type);
wire_sync_write(REQ_FD, take(msg));
msg = wire_sync_read(tmpctx, REQ_FD);
diff --git a/openingd/dualopend_wire.csv b/openingd/dualopend_wire.csv
index 2d0479f5..1c2589ab 100644
--- a/openingd/dualopend_wire.csv
+++ b/openingd/dualopend_wire.csv
@@ -98,6 +98,7 @@ msgdata,dualopend_got_offer,shutdown_scriptpubkey,u8,shutdown_len
msgdata,dualopend_got_offer,requested_amt,?amount_sat,
msgdata,dualopend_got_offer,lease_blockheight_start,u32,
msgdata,dualopend_got_offer,require_confirmed_inputs,bool,
+msgdata,dualopend_got_offer,ctype,channel_type,
# master->dualopend: reply back with our first funding info/contribs
msgtype,dualopend_got_offer_reply,7105
diff --git a/openingd/openingd.c b/openingd/openingd.c
index e98609d0..1990a9d2 100644
--- a/openingd/openingd.c
+++ b/openingd/openingd.c
@@ -1009,7 +1009,8 @@ static u8 *fundee_channel(struct state *state, const u8 *open_channel_msg)
state->remoteconf.to_self_delay,
state->remoteconf.max_accepted_htlcs,
channel_flags,
- state->upfront_shutdown_script[REMOTE]);
+ state->upfront_shutdown_script[REMOTE],
+ state->channel_type);
wire_sync_write(REQ_FD, take(msg));
msg = wire_sync_read(tmpctx, REQ_FD);
diff --git a/openingd/openingd_wire.csv b/openingd/openingd_wire.csv
index c48e62b1..c70de6cf 100644
--- a/openingd/openingd_wire.csv
+++ b/openingd/openingd_wire.csv
@@ -44,6 +44,7 @@ msgdata,openingd_got_offer,max_accepted_htlcs,u16,
msgdata,openingd_got_offer,channel_flags,u8,
msgdata,openingd_got_offer,shutdown_len,u16,
msgdata,openingd_got_offer,shutdown_scriptpubkey,u8,shutdown_len
+msgdata,openingd_got_offer,ctype,channel_type,
# master->openingd: optional rejection message
msgtype,openingd_got_offer_reply,6105
diff --git a/tests/plugins/openchannel_hook_accepter.py b/tests/plugins/openchannel_hook_accepter.py
index be409fa4..58c98812 100755
--- a/tests/plugins/openchannel_hook_accepter.py
+++ b/tests/plugins/openchannel_hook_accepter.py
@@ -16,7 +16,7 @@ from pyln.client import Plugin, Millisatoshi
plugin = Plugin()
-def run_openchannel(funding_sats_str, plugin):
+def run_openchannel(funding_sats_str, ctype, plugin):
# Convert from string to satoshis
funding_sats = Millisatoshi(funding_sats_str).to_satoshi()
@@ -46,19 +46,19 @@ def run_openchannel(funding_sats_str, plugin):
return {'result': 'continue', 'close_to': 'bc1qlq8srqnz64wgklmqvurv7qnr4rvtq2u96hhfg2'}
# - otherwise: accept and don't include the close_to
- plugin.log("accept by design")
+ plugin.log(f"accept by design: channel_type {ctype}")
return {'result': 'continue'}
@plugin.hook('openchannel')
def on_openchannel(openchannel, plugin, **kwargs):
- return run_openchannel(openchannel['funding_msat'], plugin)
+ return run_openchannel(openchannel['funding_msat'], openchannel['channel_type'], plugin)
@plugin.hook('openchannel2')
def on_openchannel2(openchannel2, plugin, **kwargs):
""" Support for v2 channel opens """
- return run_openchannel(openchannel2['their_funding_msat'], plugin)
+ return run_openchannel(openchannel2['their_funding_msat'], openchannel2['channel_type'], plugin)
plugin.run()
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index d187ac61..d1c6f293 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -695,8 +695,10 @@ def test_openchannel_hook(node_factory, bitcoind):
if 'anchors/even' in only_one(l1.rpc.listpeerchannels()['channels'])['channel_type']['names']:
feerate = 3750
+ expected['channel_type'] = r"{'bits': \[12, 22\], 'names': \['static_remotekey/even', 'anchors/even'\]}"
else:
feerate = 7500
+ expected['channel_type'] = r"{'bits': \[12\], 'names': \['static_remotekey/even'\]}"
if l2.config('experimental-dual-fund'):
# openchannel2 var checks
expected.update({
@@ -4370,3 +4372,17 @@ def test_peer_storage(node_factory, bitcoind):
# This should never happen
assert not l1.daemon.is_in_log(r'PeerStorageFailed')
assert not l2.daemon.is_in_log(r'PeerStorageFailed')
+
+
+@pytest.mark.openchannel('v1')
+@pytest.mark.openchannel('v2')
+def test_openchannel_hook_channel_type(node_factory, bitcoind):
+ """openchannel hook should get a channel_type field.
+ """
+ opts = {'plugin': os.path.join(os.getcwd(), 'tests/plugins/openchannel_hook_accepter.py')}
+ l1, l2 = node_factory.line_graph(2, opts=opts)
+
+ if 'anchors/even' in only_one(l1.rpc.listpeerchannels()['channels'])['channel_type']['names']:
+ l2.daemon.wait_for_log(r"plugin-openchannel_hook_accepter.py: accept by design: channel_type {'bits': \[12, 22\], 'names': \['static_remotekey/even', 'anchors/even'\]}")
+ else:
+ l2.daemon.wait_for_log(r"plugin-openchannel_hook_accepter.py: accept by design: channel_type {'bits': \[12\], 'names': \['static_remotekey/even'\]}")
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.