lightningd: always tell openingd/dualopend what channel type we want.
What changed, and why it matters
This commit changes how Core Lightning decides what kind of channel to open with another node. Previously, the opening daemon could fall back to a default channel type if none was specified. Now, the main lightningd process always picks and sends a specific desired channel type. A side effect is that if the peer signals support for zero-confirmation channels (by setting minimum_depth to 0), the returned channel type will include the zeroconf feature even if the user did not ask for it. The commit is described as a behavior change, not a security fix, and there is no evidence of a disclosed vulnerability.
Treat as a normal feature/behavior change. Review whether automatically including option_zeroconf based on peer minimum_depth is acceptable for your deployment's trust model, as it changes user-visible RPC output and channel semantics. No urgent security patch appears warranted based on the supplied materials.
Security signals we found
Behavior change in channel type negotiation: zeroconf now implied by peer's minimum_depth=0 even if not explicitly requested
Channel type field made mandatory in inter-daemon wire messages, removing silent defaults
SCID_ALIAS now added based on feature negotiation plus unannounced flag, not only when default channel type was used
No bounds checks, memory safety fixes, or authentication changes visible in diff
Evidence from the diff
The patch removes default_channel_type() from common code and adds desired_channel_type() in lightningd, ensuring channel_type is always computed by lightningd and passed as a required (non-optional) field to openingd and dualopend. The wire protocol fields change from ?channel_type to channel_type. In openingd, the logic for adding option_scid_alias and option_zeroconf is adjusted: SCID_ALIAS is added when negotiated and the channel is unannounced; ZERCONF is added when the peer’s minimum_depth is 0, regardless of whether an explicit channel type was provided. Tests are updated to expect ZERCONF in returned channel types even when not requested.
Changed components
lightningd channel type selectionopeningd channel opening negotiationdualopend dual-funded channel openingJSON-RPC fundchannel / fundchannel_start responsescommon channel_type helper codeInspect captured patch +53 / −83
diff --git a/common/channel_type.c b/common/channel_type.c
index 49c7e32d..61302b45 100644
--- a/common/channel_type.c
+++ b/common/channel_type.c
@@ -72,26 +72,6 @@ struct channel_type *channel_type_anchors_zero_fee_htlc(const tal_t *ctx)
return type;
}
-struct channel_type *default_channel_type(const tal_t *ctx,
- const struct feature_set *our_features,
- const u8 *their_features)
-{
- /* BOLT #2:
- * Both peers:
- * - if `channel_type` was present in both `open_channel` and `accept_channel`:
- * - This is the `channel_type` (they must be equal, required above)
- * - otherwise:
- * - if `option_anchors` was negotiated:
- * - the `channel_type` is `option_anchors` and `option_static_remotekey` (bits 22 and 12)
- * - otherwise:
- * - the `channel_type` is `option_static_remotekey` (bit 12)
- */
- if (feature_negotiated(our_features, their_features,
- OPT_ANCHORS_ZERO_FEE_HTLC_TX))
- return channel_type_anchors_zero_fee_htlc(ctx);
- return channel_type_static_remotekey(ctx);
-}
-
bool channel_type_has(const struct channel_type *type, int feature)
{
return feature_offered(type->features, feature);
diff --git a/common/channel_type.h b/common/channel_type.h
index 115c394f..d2673eb3 100644
--- a/common/channel_type.h
+++ b/common/channel_type.h
@@ -21,11 +21,6 @@ struct channel_type *channel_type_dup(const tal_t *ctx,
struct channel_type *channel_type_from(const tal_t *ctx,
const u8 *features TAKES);
-/* Derive channel type from feature negotiation */
-struct channel_type *default_channel_type(const tal_t *ctx,
- const struct feature_set *our_features,
- const u8 *their_features);
-
/* Does this type include this feature? */
bool channel_type_has(const struct channel_type *type, int feature);
diff --git a/lightningd/channel.c b/lightningd/channel.c
index 94ff06c7..346cb1c4 100644
--- a/lightningd/channel.c
+++ b/lightningd/channel.c
@@ -242,6 +242,16 @@ struct open_attempt *new_channel_open_attempt(struct channel *channel)
return oa;
}
+struct channel_type *desired_channel_type(const tal_t *ctx,
+ const struct feature_set *our_features,
+ const u8 *their_features)
+{
+ if (feature_negotiated(our_features, their_features,
+ OPT_ANCHORS_ZERO_FEE_HTLC_TX))
+ return channel_type_anchors_zero_fee_htlc(ctx);
+ return channel_type_static_remotekey(ctx);
+}
+
struct channel *new_unsaved_channel(struct peer *peer,
u32 feerate_base,
u32 feerate_ppm)
@@ -305,7 +315,7 @@ struct channel *new_unsaved_channel(struct peer *peer,
channel->close_blockheight = NULL;
/* In case someone looks at channels before open negotiation,
* initialize this with default */
- channel->type = default_channel_type(channel,
+ channel->type = desired_channel_type(channel,
ld->our_features,
peer->their_features);
diff --git a/lightningd/channel.h b/lightningd/channel.h
index f133dd2b..509c93fc 100644
--- a/lightningd/channel.h
+++ b/lightningd/channel.h
@@ -899,5 +899,9 @@ const u8 *channel_update_for_error(const tal_t *ctx,
struct amount_msat htlc_max_possible_send(const struct channel *channel);
+/* Given features, what channel_type do we want? */
+struct channel_type *desired_channel_type(const tal_t *ctx,
+ const struct feature_set *our_features,
+ const u8 *their_features);
#endif /* LIGHTNING_LIGHTNINGD_CHANNEL_H */
diff --git a/lightningd/dual_open_control.c b/lightningd/dual_open_control.c
index b96ac946..4560f9f8 100644
--- a/lightningd/dual_open_control.c
+++ b/lightningd/dual_open_control.c
@@ -3249,8 +3249,11 @@ static struct command_result *json_openchannel_init(struct command *cmd,
"by peer");
}
- if (info->ctype &&
- !cmd->ld->dev_any_channel_type &&
+ if (!info->ctype)
+ info->ctype = desired_channel_type(info, cmd->ld->our_features,
+ peer->their_features);
+
+ if (!cmd->ld->dev_any_channel_type &&
!channel_type_accept(tmpctx,
info->ctype->features,
cmd->ld->our_features)) {
@@ -3882,7 +3885,9 @@ static struct command_result *json_queryrates(struct command *cmd,
NULL : request_amt,
get_block_height(cmd->ld->topology),
true,
- NULL, NULL);
+ desired_channel_type(tmpctx, cmd->ld->our_features,
+ peer->their_features),
+ NULL);
if (socketpair(AF_LOCAL, SOCK_STREAM, 0, fds) != 0) {
return command_fail(cmd, FUND_MAX_EXCEEDED,
diff --git a/lightningd/opening_control.c b/lightningd/opening_control.c
index f3e14005..43163835 100644
--- a/lightningd/opening_control.c
+++ b/lightningd/opening_control.c
@@ -47,8 +47,7 @@ void json_add_uncommitted_channel(struct command *cmd,
json_object_start(response, NULL);
json_add_node_id(response, "peer_id", &peer->id);
json_add_bool(response, "peer_connected", peer->connected == PEER_CONNECTED);
- if (uc->fc->channel_type)
- json_add_channel_type(response, "channel_type", uc->fc->channel_type);
+ json_add_channel_type(response, "channel_type", uc->fc->channel_type);
json_add_string(response, "state", "OPENINGD");
json_add_string(response, "owner", "lightning_openingd");
json_add_string(response, "opener", "local");
@@ -332,6 +331,10 @@ static void opening_funder_start_replied(struct subd *openingd, const u8 *resp,
{
bool supports_shutdown_script;
+ /* It will tell us the resulting channel type (which can vary
+ * by ZEROCONF and SCID_ALIAS), so free old one */
+ tal_free(fc->channel_type);
+
if (!fromwire_openingd_funder_start_reply(fc, resp,
&fc->funding_scriptpubkey,
&supports_shutdown_script,
@@ -1297,7 +1300,7 @@ static struct command_result *json_fundchannel_start(struct command *cmd,
}
}
} else {
- fc->channel_type = NULL;
+ fc->channel_type = NULL; /* set later */
}
if (!mindepth)
@@ -1389,6 +1392,11 @@ static struct command_result *json_fundchannel_start(struct command *cmd,
if (command_check_only(cmd))
return command_check_done(cmd);
+ /* Now we know the peer, we can derive the channel type to ask for */
+ if (!fc->channel_type)
+ fc->channel_type = desired_channel_type(fc, cmd->ld->our_features,
+ peer->their_features);
+
fc->push = push_msat ? *push_msat : AMOUNT_MSAT(0);
fc->channel_flags = OUR_CHANNEL_FLAGS;
if (!*announce_channel) {
@@ -1426,7 +1434,7 @@ static struct command_result *json_fundchannel_start(struct command *cmd,
&tmp_channel_id,
fc->channel_flags,
reserve,
- ctype);
+ fc->channel_type);
if (!topology_synced(cmd->ld->topology)) {
struct fundchannel_start_info *info
diff --git a/openingd/dualopend.c b/openingd/dualopend.c
index 616df5dd..f6c183a9 100644
--- a/openingd/dualopend.c
+++ b/openingd/dualopend.c
@@ -2961,7 +2961,6 @@ static void opener_start(struct state *state, u8 *msg)
struct amount_sat *requested_lease;
size_t locktime;
u32 nonanchor_feerate, anchor_feerate;
- struct channel_type *ctype;
if (!fromwire_dualopend_opener_init(state, msg,
&tx_state->psbt,
@@ -2975,7 +2974,7 @@ static void opener_start(struct state *state, u8 *msg)
&requested_lease,
&tx_state->blockheight,
&dry_run,
- &ctype,
+ &state->channel_type,
&expected_rates))
master_badmsg(WIRE_DUALOPEND_OPENER_INIT, msg);
@@ -2983,22 +2982,6 @@ static void opener_start(struct state *state, u8 *msg)
wally_psbt_get_locktime(tx_state->psbt, &locktime);
tx_state->tx_locktime = locktime;
open_tlv = tlv_opening_tlvs_new(tmpctx);
-
- /* BOLT #2:
- * - if it includes `channel_type`:
- * - MUST set it to a defined type representing the type it wants.
- * - MUST use the smallest bitmap possible to represent the channel
- * type.
- * - SHOULD NOT set it to a type containing a feature which was not
- * negotiated.
- */
- if (ctype) {
- state->channel_type = ctype;
- } else {
- state->channel_type = default_channel_type(state,
- state->our_features,
- state->their_features);
- }
open_tlv->channel_type = state->channel_type->features;
/* Given channel type, which feerate do we use? */
diff --git a/openingd/dualopend_wire.csv b/openingd/dualopend_wire.csv
index 8d891502..2d0479f5 100644
--- a/openingd/dualopend_wire.csv
+++ b/openingd/dualopend_wire.csv
@@ -211,7 +211,7 @@ msgdata,dualopend_opener_init,channel_flags,u8,
msgdata,dualopend_opener_init,requested_sats,?amount_sat,
msgdata,dualopend_opener_init,blockheight,u32,
msgdata,dualopend_opener_init,dry_run,bool,
-msgdata,dualopend_opener_init,channel_type,?channel_type,
+msgdata,dualopend_opener_init,channel_type,channel_type,
# must go last because embedded tu32
msgdata,dualopend_opener_init,expected_rates,?lease_rates,
diff --git a/openingd/openingd.c b/openingd/openingd.c
index f4361770..60faefde 100644
--- a/openingd/openingd.c
+++ b/openingd/openingd.c
@@ -294,7 +294,7 @@ static bool intuit_scid_alias_type(struct state *state, u8 channel_flags)
* stop when we get to the part where we need the funding txid */
static u8 *funder_channel_start(struct state *state, u8 channel_flags,
u32 nonanchor_feerate, u32 anchor_feerate,
- const struct channel_type *ctype)
+ const struct channel_type *ctype TAKES)
{
u8 *msg;
u8 *funding_output_script;
@@ -323,28 +323,12 @@ static u8 *funder_channel_start(struct state *state, u8 channel_flags,
state->our_features,
state->their_features);
- if (ctype) {
- state->channel_type = channel_type_dup(state, ctype);
- } else {
- state->channel_type = default_channel_type(state,
- state->our_features,
- state->their_features);
-
- /* Spec says we should use the option_scid_alias variation if we
- * want them to *only* use the scid_alias (which we do for unannounced
- * channels!).
- *
- * But:
- * 1. We didn't accept this in CLN prior to v23.05.
- * 2. LND won't accept that without OPT_ANCHORS_ZERO_FEE_HTLC_TX.
- * 3. LND <= 18 won't accept OPT_SCID_ALIAS unless it advertizes it,
- * which it does not by default.
- */
- if (channel_type_has(state->channel_type, OPT_ANCHORS_ZERO_FEE_HTLC_TX)
- && feature_offered(state->their_features, OPT_SCID_ALIAS)) {
- if (!(channel_flags & CHANNEL_FLAGS_ANNOUNCE_CHANNEL))
- channel_type_set_scid_alias(state->channel_type);
- }
+ state->channel_type = channel_type_dup(state, ctype);
+ /* We set scid alias if we're not announcing */
+ if (feature_negotiated(state->our_features, state->their_features,
+ OPT_SCID_ALIAS)
+ && !(channel_flags & CHANNEL_FLAGS_ANNOUNCE_CHANNEL)) {
+ channel_type_set_scid_alias(state->channel_type);
}
/* Which feerate do we use? (We can lowball fees if using anchors!) */
@@ -569,7 +553,7 @@ static u8 *funder_channel_start(struct state *state, u8 channel_flags,
"We negotiated option_zeroconf, using our minimum_depth=%d",
state->minimum_depth);
/* We set this now to show we're zeroconf */
- if (their_mindepth == 0 && !ctype)
+ if (their_mindepth == 0)
channel_type_set_zeroconf(state->channel_type);
} else {
state->minimum_depth = their_mindepth;
diff --git a/openingd/openingd_wire.csv b/openingd/openingd_wire.csv
index 65aac949..c48e62b1 100644
--- a/openingd/openingd_wire.csv
+++ b/openingd/openingd_wire.csv
@@ -88,7 +88,7 @@ msgdata,openingd_funder_start,anchor_feerate_per_kw,u32,
msgdata,openingd_funder_start,temporary_channel_id,channel_id,
msgdata,openingd_funder_start,channel_flags,u8,
msgdata,openingd_funder_start,reserve,?amount_sat,
-msgdata,openingd_funder_start,channel_type,?channel_type,
+msgdata,openingd_funder_start,channel_type,channel_type,
# openingd->master: send back output script for 2-of-2 funding output
msgtype,openingd_funder_start_reply,6102
diff --git a/tests/test_opening.py b/tests/test_opening.py
index eb8a4ced..f91f1745 100644
--- a/tests/test_opening.py
+++ b/tests/test_opening.py
@@ -2614,8 +2614,9 @@ def test_opening_explicit_channel_type(node_factory, bitcoind):
[STATIC_REMOTEKEY, ANCHORS_ZERO_FEE_HTLC_TX]):
ret = l1.rpc.fundchannel_start(l2.info['id'], FUNDAMOUNT,
channel_type=ctype + zeroconf)
- assert ret['channel_type']['bits'] == ctype + zeroconf
- assert only_one(l1.rpc.listpeerchannels()['channels'])['channel_type']['bits'] == ctype + zeroconf
+ # We get zeroconf even without asking for it.
+ assert ret['channel_type']['bits'] == ctype + [ZEROCONF]
+ assert only_one(l1.rpc.listpeerchannels()['channels'])['channel_type']['bits'] == ctype + [ZEROCONF]
# Note: l2 doesn't show it in listpeerchannels yet...
l1.rpc.fundchannel_cancel(l2.info['id'])
@@ -2664,8 +2665,8 @@ def test_opening_explicit_channel_type(node_factory, bitcoind):
l1.connect(l2)
ret = l1.rpc.fundchannel_start(l2.info['id'], FUNDAMOUNT, channel_type=[STATIC_REMOTEKEY, ANCHORS_OLD])
- assert ret['channel_type']['bits'] == [STATIC_REMOTEKEY, ANCHORS_OLD]
- assert only_one(l1.rpc.listpeerchannels()['channels'])['channel_type']['bits'] == [STATIC_REMOTEKEY, ANCHORS_OLD]
+ assert ret['channel_type']['bits'] == [STATIC_REMOTEKEY, ANCHORS_OLD, ZEROCONF]
+ assert only_one(l1.rpc.listpeerchannels()['channels'])['channel_type']['bits'] == [STATIC_REMOTEKEY, ANCHORS_OLD, ZEROCONF]
# Note: l3 doesn't show it in listpeerchannels yet...
l1.rpc.fundchannel_cancel(l2.info['id'])
@@ -2673,8 +2674,8 @@ def test_opening_explicit_channel_type(node_factory, bitcoind):
# Works with fundchannel / multifundchannel
ret = l1.rpc.fundchannel(l2.info['id'], FUNDAMOUNT // 3, channel_type=[STATIC_REMOTEKEY])
- assert ret['channel_type']['bits'] == [STATIC_REMOTEKEY]
- assert only_one(l1.rpc.listpeerchannels()['channels'])['channel_type']['bits'] == [STATIC_REMOTEKEY]
+ assert ret['channel_type']['bits'] == [STATIC_REMOTEKEY, ZEROCONF]
+ assert only_one(l1.rpc.listpeerchannels()['channels'])['channel_type']['bits'] == [STATIC_REMOTEKEY, ZEROCONF]
assert only_one(l2.rpc.listpeerchannels()['channels'])['channel_type']['bits'] == [STATIC_REMOTEKEY]
# FIXME: Check type is actually correct!
Why this scored 27/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.