lightningd: save funding PSBT to database if we're to withhold it.
What changed, and why it matters
This commit adds plumbing to remember whether a new Lightning channel's funding transaction is being deliberately withheld from broadcast, and to store the unsigned funding PSBT in the database so it can be signed and broadcast later. The actual feature is not yet enabled for users (a comment says 'FIXME: Set by option' and the flag is hard-coded to false). It is a preparatory/refactoring change, not a fix for an active security bug.
No immediate action required. Treat as normal feature development. If deploying from this commit, note that the withholding feature is not yet user-configurable and the funding PSBT storage path should be reviewed once the option is implemented and exposed.
Security signals we found
New database column and persistence path for funding PSBT tied to a withholding flag
Hard-coded false value with FIXME suggests feature is incomplete and not exposed
No input validation or access-control changes visible in the diff
No mention of vulnerability, CVE, or security issue in commit message
Evidence from the diff
The change introduces a ‘withheld’ boolean on struct channel, struct funding_channel, and struct closed_channel, plus a corresponding ‘withheld’ column in the channels table. It threads the flag through channel creation, wallet load/save, and closed-channel queries. The only runtime assignment is ‘fc->withheld = false’ in json_fundchannel_complete, with a FIXME indicating a future option. The funding PSBT is now persisted when withholding is intended so the node can later sign and broadcast the funding transaction.
Changed components
lightningd/channel.clightningd/channel.hlightningd/closed_channel.hlightningd/opening_common.hlightningd/opening_control.cwallet/db.cwallet/wallet.cInspect captured patch +44 / −13
diff --git a/lightningd/channel.c b/lightningd/channel.c
index 53f2ebc..bd2f4de 100644
--- a/lightningd/channel.c
+++ b/lightningd/channel.c
@@ -436,6 +436,7 @@ struct channel *new_unsaved_channel(struct peer *peer,
channel->channel_gossip = NULL;
channel->forgets = tal_arr(channel, struct command *, 0);
channel->funding_psbt = NULL;
+ channel->withheld = false;
list_add_tail(&peer->channels, &channel->list);
channel->rr_number = peer->ld->rr_counter++;
tal_add_destructor(channel, destroy_channel);
@@ -558,7 +559,8 @@ struct channel *new_channel(struct peer *peer, u64 dbid,
u64 last_stable_connection,
const struct channel_stats *stats,
struct channel_state_change **state_changes STEALS,
- const struct wally_psbt *funding_psbt STEALS)
+ const struct wally_psbt *funding_psbt STEALS,
+ bool withheld)
{
struct channel *channel = tal(peer->ld, struct channel);
struct amount_msat htlc_min, htlc_max;
@@ -735,6 +737,7 @@ struct channel *new_channel(struct peer *peer, u64 dbid,
&channel->cid,
"We can't be together anymore.");
channel->funding_psbt = tal_steal(channel, funding_psbt);
+ channel->withheld = withheld;
return channel;
}
diff --git a/lightningd/channel.h b/lightningd/channel.h
index 60e712d..bb27b68 100644
--- a/lightningd/channel.h
+++ b/lightningd/channel.h
@@ -359,6 +359,9 @@ struct channel {
/* Unsigned PSBT if we initiated the open channel */
const struct wally_psbt *funding_psbt;
+
+ /* Are we not broadcasting the open tx? */
+ bool withheld;
};
/* Is channel owned (and should be talking to peer) */
@@ -445,7 +448,8 @@ struct channel *new_channel(struct peer *peer, u64 dbid,
u64 last_stable_connection,
const struct channel_stats *stats,
struct channel_state_change **state_changes STEALS,
- const struct wally_psbt *funding_psbt STEALS);
+ const struct wally_psbt *funding_psbt STEALS,
+ bool withheld);
/* new_inflight - Create a new channel_inflight for a channel */
struct channel_inflight *new_inflight(struct channel *channel,
diff --git a/lightningd/closed_channel.h b/lightningd/closed_channel.h
index cf7c91b..68822bc 100644
--- a/lightningd/closed_channel.h
+++ b/lightningd/closed_channel.h
@@ -31,6 +31,7 @@ struct closed_channel {
/* NULL for older closed channels */
const struct shachain *their_shachain;
const struct wally_psbt *funding_psbt;
+ bool withheld;
};
static inline const struct channel_id *keyof_closed_channel(const struct closed_channel *cc)
diff --git a/lightningd/opening_common.h b/lightningd/opening_common.h
index 9cf112e..0250b56 100644
--- a/lightningd/opening_common.h
+++ b/lightningd/opening_common.h
@@ -104,6 +104,9 @@ struct funding_channel {
/* Were we the one to publish the commitment/splicing tx? */
const struct wally_psbt *funding_psbt;
+
+ /* Were we told to withhold the commitment tx? */
+ bool withheld;
};
struct uncommitted_channel *new_uncommitted_channel(struct peer *peer);
diff --git a/lightningd/opening_control.c b/lightningd/opening_control.c
index 4968c48..c1ef2b7 100644
--- a/lightningd/opening_control.c
+++ b/lightningd/opening_control.c
@@ -97,7 +97,8 @@ wallet_commit_channel(struct lightningd *ld,
const u8 *our_upfront_shutdown_script,
const u8 *remote_upfront_shutdown_script,
const struct channel_type *type,
- const struct wally_psbt *funding_psbt)
+ const struct wally_psbt *funding_psbt,
+ bool withheld)
{
struct channel *channel;
struct amount_msat our_msat;
@@ -237,7 +238,8 @@ wallet_commit_channel(struct lightningd *ld,
0,
&zero_channel_stats,
tal_arr(NULL, struct channel_state_change *, 0),
- funding_psbt);
+ funding_psbt,
+ withheld);
/* Now we finally put it in the database. */
wallet_channel_insert(ld->wallet, channel);
@@ -446,7 +448,8 @@ static void opening_funder_finished(struct subd *openingd, const u8 *resp,
fc->our_upfront_shutdown_script,
remote_upfront_shutdown_script,
type,
- fc->funding_psbt);
+ fc->funding_psbt,
+ fc->withheld);
if (!channel) {
was_pending(command_fail(fc->cmd, LIGHTNINGD,
"Key generation failure"));
@@ -550,7 +553,8 @@ static void opening_fundee_finished(struct subd *openingd,
local_upfront_shutdown_script,
remote_upfront_shutdown_script,
type,
- NULL);
+ NULL,
+ false);
if (!channel) {
uncommitted_channel_disconnect(uc, LOG_BROKEN,
"Commit channel failed");
@@ -1096,6 +1100,9 @@ static struct command_result *json_fundchannel_complete(struct command *cmd,
fc->funding_psbt = tal_steal(fc, funding_psbt);
+ /* FIXME: Set by option */
+ fc->withheld = false;
+
/* Set the cmd to this new cmd */
peer->uncommitted_channel->fc->cmd = cmd;
msg = towire_openingd_funder_complete(NULL,
@@ -1644,7 +1651,8 @@ static struct channel *stub_chan(struct command *cmd,
0,
&zero_channel_stats,
tal_arr(NULL, struct channel_state_change *, 0),
- NULL);
+ NULL,
+ false);
/* We don't want to gossip about this, ever. */
channel->channel_gossip = tal_free(channel->channel_gossip);
diff --git a/wallet/db.c b/wallet/db.c
index 0024092..a038722 100644
--- a/wallet/db.c
+++ b/wallet/db.c
@@ -1101,6 +1101,7 @@ static struct migration dbmigrations[] = {
" PRIMARY KEY (id)"
")"), NULL},
{NULL, migrate_fail_pending_payments_without_htlcs},
+ {SQL("ALTER TABLE channels ADD withheld INTEGER DEFAULT 0;"), NULL},
};
/**
diff --git a/wallet/test/run-chain_moves_duplicate-detect.c b/wallet/test/run-chain_moves_duplicate-detect.c
index 2eaaa73..63a68ad 100644
--- a/wallet/test/run-chain_moves_duplicate-detect.c
+++ b/wallet/test/run-chain_moves_duplicate-detect.c
@@ -235,7 +235,8 @@ struct channel *new_channel(struct peer *peer UNNEEDED, u64 dbid UNNEEDED,
u64 last_stable_connection UNNEEDED,
const struct channel_stats *stats UNNEEDED,
struct channel_state_change **state_changes STEALS UNNEEDED,
- const struct wally_psbt *funding_psbt STEALS UNNEEDED)
+ const struct wally_psbt *funding_psbt STEALS UNNEEDED,
+ bool withheld UNNEEDED)
{ fprintf(stderr, "new_channel called!\n"); abort(); }
/* Generated stub for new_channel_state_change */
struct channel_state_change *new_channel_state_change(const tal_t *ctx UNNEEDED,
diff --git a/wallet/test/run-db.c b/wallet/test/run-db.c
index 26d4280..6ab8e6e 100644
--- a/wallet/test/run-db.c
+++ b/wallet/test/run-db.c
@@ -243,7 +243,8 @@ struct channel *new_channel(struct peer *peer UNNEEDED, u64 dbid UNNEEDED,
u64 last_stable_connection UNNEEDED,
const struct channel_stats *stats UNNEEDED,
struct channel_state_change **state_changes STEALS UNNEEDED,
- const struct wally_psbt *funding_psbt STEALS UNNEEDED)
+ const struct wally_psbt *funding_psbt STEALS UNNEEDED,
+ bool withheld UNNEEDED)
{ fprintf(stderr, "new_channel called!\n"); abort(); }
/* Generated stub for new_channel_state_change */
struct channel_state_change *new_channel_state_change(const tal_t *ctx UNNEEDED,
diff --git a/wallet/test/run-migrate_remove_chain_moves_duplicates.c b/wallet/test/run-migrate_remove_chain_moves_duplicates.c
index 9a93d2c..480e283 100644
--- a/wallet/test/run-migrate_remove_chain_moves_duplicates.c
+++ b/wallet/test/run-migrate_remove_chain_moves_duplicates.c
@@ -250,7 +250,8 @@ struct channel *new_channel(struct peer *peer UNNEEDED, u64 dbid UNNEEDED,
u64 last_stable_connection UNNEEDED,
const struct channel_stats *stats UNNEEDED,
struct channel_state_change **state_changes STEALS UNNEEDED,
- const struct wally_psbt *funding_psbt STEALS UNNEEDED)
+ const struct wally_psbt *funding_psbt STEALS UNNEEDED,
+ bool withheld UNNEEDED)
{ fprintf(stderr, "new_channel called!\n"); abort(); }
/* Generated stub for new_channel_state_change */
struct channel_state_change *new_channel_state_change(const tal_t *ctx UNNEEDED,
diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c
index f559a76..dd5856f 100644
--- a/wallet/test/run-wallet.c
+++ b/wallet/test/run-wallet.c
@@ -1625,7 +1625,8 @@ static bool test_channel_inflight_crud(struct lightningd *ld, const tal_t *ctx)
0,
stats,
tal_arr(NULL, struct channel_state_change *, 0),
- NULL);
+ NULL,
+ false);
db_begin_transaction(w->db);
CHECK(!wallet_err);
wallet_channel_insert(w, chan);
diff --git a/wallet/wallet.c b/wallet/wallet.c
index 5c561f8..a979158 100644
--- a/wallet/wallet.c
+++ b/wallet/wallet.c
@@ -2184,7 +2184,8 @@ static struct channel *wallet_stmt2channel(struct wallet *w, struct db_stmt *stm
db_col_u64(stmt, "last_stable_connection"),
&stats,
state_changes,
- funding_psbt);
+ funding_psbt,
+ db_col_int(stmt, "withheld"));
if (!wallet_channel_load_inflights(w, chan)) {
tal_free(chan);
@@ -2242,6 +2243,7 @@ static struct closed_channel *wallet_stmt2closed_channel(const tal_t *ctx,
cc->funding_psbt = db_col_psbt(cc, stmt, "funding_psbt");
else
cc->funding_psbt = NULL;
+ cc->withheld = db_col_int(stmt, "withheld");
return cc;
}
@@ -2278,6 +2280,7 @@ void wallet_load_closed_channels(struct wallet *w,
", last_stable_connection"
", shachain_remote_id"
", funding_psbt"
+ ", withheld"
" FROM channels"
" LEFT JOIN peers p ON p.id = peer_id"
" WHERE state = ?;"));
@@ -2324,6 +2327,7 @@ void wallet_load_one_closed_channel(struct wallet *w,
", last_stable_connection"
", shachain_remote_id"
", funding_psbt"
+ ", withheld"
" FROM channels"
" LEFT JOIN peers p ON p.id = peer_id"
" WHERE channels.id = ?;"));
@@ -2444,6 +2448,7 @@ static bool wallet_channels_load_active(struct wallet *w)
", out_msatoshi_fulfilled"
", close_attempt_height"
", funding_psbt"
+ ", withheld"
" FROM channels"
" WHERE state != ?;")); //? 0
db_bind_int(stmt, CLOSED);
@@ -2696,7 +2701,8 @@ void wallet_channel_save(struct wallet *w, struct channel *chan)
" last_stable_connection=?,"
" require_confirm_inputs_remote=?,"
" close_attempt_height=?,"
- " funding_psbt=?"
+ " funding_psbt=?,"
+ " withheld=?"
" WHERE id=?"));
db_bind_u64(stmt, chan->their_shachain.id);
if (chan->scid)
@@ -2798,6 +2804,7 @@ void wallet_channel_save(struct wallet *w, struct channel *chan)
db_bind_psbt(stmt, chan->funding_psbt);
else
db_bind_null(stmt);
+ db_bind_int(stmt, chan->withheld);
db_bind_u64(stmt, chan->dbid);
db_exec_prepared_v2(take(stmt));
Why this scored 21/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.