lightningd: save funding_psbt in channel, database.
What changed, and why it matters
This commit restores the use of an existing database column called funding_psbt in Core Lightning. It stores the unsigned Bitcoin transaction (PSBT) used to fund a Lightning channel in the channel record and database. The change itself is a data plumbing fix, not an obvious security patch. There is no direct evidence in the commit that it fixes an active vulnerability, but storing sensitive transaction data securely matters because the PSBT contains details about the funds being committed.
Review whether persisted funding PSBTs contain any private key material, non-finalized signatures, or other sensitive data that should be encrypted at rest. Verify that db_col_psbt and db_bind_psbt handle NULL and malformed values safely. Consider whether this change is a prerequisite for a later security fix and monitor related follow-up commits.
Security signals we found
Restores persistence of unsigned funding PSBT in database column
Adds funding_psbt to channel, closed_channel, and funding_channel structs
Updates wallet save/load paths for active and closed channels
No explicit security framing or CVE reference in commit message
No input validation or memory-safety changes visible in diff
Evidence from the diff
The commit re-plumbs the funding_psbt field into struct channel, struct closed_channel, and struct funding_channel. It updates wallet_stmt2channel and wallet_channel_save to read/write the pre-existing funding_psbt column, and propagates the PSBT through channel creation paths in opening_control.c and dual_open_control.c. The database column already existed from 2020 but was unused after the inflight architecture replaced it. The change ensures the unsigned funding PSBT is persisted with the channel and updated when an inflight PSBT is finalized.
Changed components
lightningd/channel.clightningd/channel.hlightningd/closed_channel.hlightningd/dual_open_control.clightningd/opening_common.hlightningd/opening_control.cwallet/wallet.cInspect captured patch +61 / −14
diff --git a/lightningd/channel.c b/lightningd/channel.c
index 078827f..53f2ebc 100644
--- a/lightningd/channel.c
+++ b/lightningd/channel.c
@@ -435,6 +435,7 @@ struct channel *new_unsaved_channel(struct peer *peer,
/* channel->channel_gossip gets populated once we know if it's public. */
channel->channel_gossip = NULL;
channel->forgets = tal_arr(channel, struct command *, 0);
+ channel->funding_psbt = NULL;
list_add_tail(&peer->channels, &channel->list);
channel->rr_number = peer->ld->rr_counter++;
tal_add_destructor(channel, destroy_channel);
@@ -556,7 +557,8 @@ struct channel *new_channel(struct peer *peer, u64 dbid,
struct peer_update *peer_update STEALS,
u64 last_stable_connection,
const struct channel_stats *stats,
- struct channel_state_change **state_changes STEALS)
+ struct channel_state_change **state_changes STEALS,
+ const struct wally_psbt *funding_psbt STEALS)
{
struct channel *channel = tal(peer->ld, struct channel);
struct amount_msat htlc_min, htlc_max;
@@ -732,7 +734,7 @@ struct channel *new_channel(struct peer *peer, u64 dbid,
channel->error = towire_errorfmt(peer->ld,
&channel->cid,
"We can't be together anymore.");
-
+ channel->funding_psbt = tal_steal(channel, funding_psbt);
return channel;
}
diff --git a/lightningd/channel.h b/lightningd/channel.h
index e94ab85..60e712d 100644
--- a/lightningd/channel.h
+++ b/lightningd/channel.h
@@ -356,6 +356,9 @@ struct channel {
/* Our change history. */
struct channel_state_change **state_changes;
+
+ /* Unsigned PSBT if we initiated the open channel */
+ const struct wally_psbt *funding_psbt;
};
/* Is channel owned (and should be talking to peer) */
@@ -441,7 +444,8 @@ struct channel *new_channel(struct peer *peer, u64 dbid,
struct peer_update *peer_update STEALS,
u64 last_stable_connection,
const struct channel_stats *stats,
- struct channel_state_change **state_changes STEALS);
+ struct channel_state_change **state_changes STEALS,
+ const struct wally_psbt *funding_psbt STEALS);
/* 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 fc8382d..cf7c91b 100644
--- a/lightningd/closed_channel.h
+++ b/lightningd/closed_channel.h
@@ -30,6 +30,7 @@ struct closed_channel {
u64 last_stable_connection;
/* NULL for older closed channels */
const struct shachain *their_shachain;
+ const struct wally_psbt *funding_psbt;
};
static inline const struct channel_id *keyof_closed_channel(const struct closed_channel *cc)
diff --git a/lightningd/dual_open_control.c b/lightningd/dual_open_control.c
index cb7c28b..388ba47 100644
--- a/lightningd/dual_open_control.c
+++ b/lightningd/dual_open_control.c
@@ -1501,6 +1501,8 @@ wallet_commit_channel(struct lightningd *ld,
channel->lease_chan_max_ppt = lease_chan_max_ppt;
channel->htlc_minimum_msat = channel_info->their_config.htlc_minimum;
channel->htlc_maximum_msat = htlc_max_possible_send(channel);
+ /* Filled in when we have PSBT for inflight */
+ channel->funding_psbt = NULL;
/* Now we finally put it in the database. */
wallet_channel_insert(ld->wallet, channel);
@@ -2773,6 +2775,11 @@ json_openchannel_signed(struct command *cmd,
wallet_inflight_save(cmd->ld->wallet, inflight);
watch_opening_inflight(cmd->ld, inflight);
+ /* Channel's funding psbt also updated now */
+ tal_free(channel->funding_psbt);
+ channel->funding_psbt = clone_psbt(channel, inflight->funding_psbt);
+ wallet_channel_save(cmd->ld->wallet, channel);
+
/* Only after we've updated/saved our psbt do we check
* for peer connected */
if (!channel->owner)
diff --git a/lightningd/opening_common.h b/lightningd/opening_common.h
index 21ca822..9cf112e 100644
--- a/lightningd/opening_common.h
+++ b/lightningd/opening_common.h
@@ -101,6 +101,9 @@ struct funding_channel {
/* Place to stash the per-peer-state while we wait
* for them to get back to us with signatures */
struct peer_fd *peer_fd;
+
+ /* Were we the one to publish the commitment/splicing tx? */
+ const struct wally_psbt *funding_psbt;
};
struct uncommitted_channel *new_uncommitted_channel(struct peer *peer);
diff --git a/lightningd/opening_control.c b/lightningd/opening_control.c
index eaa806c..4968c48 100644
--- a/lightningd/opening_control.c
+++ b/lightningd/opening_control.c
@@ -96,7 +96,8 @@ wallet_commit_channel(struct lightningd *ld,
u32 feerate,
const u8 *our_upfront_shutdown_script,
const u8 *remote_upfront_shutdown_script,
- const struct channel_type *type)
+ const struct channel_type *type,
+ const struct wally_psbt *funding_psbt)
{
struct channel *channel;
struct amount_msat our_msat;
@@ -235,7 +236,8 @@ wallet_commit_channel(struct lightningd *ld,
NULL,
0,
&zero_channel_stats,
- tal_arr(NULL, struct channel_state_change *, 0));
+ tal_arr(NULL, struct channel_state_change *, 0),
+ funding_psbt);
/* Now we finally put it in the database. */
wallet_channel_insert(ld->wallet, channel);
@@ -443,7 +445,8 @@ static void opening_funder_finished(struct subd *openingd, const u8 *resp,
feerate,
fc->our_upfront_shutdown_script,
remote_upfront_shutdown_script,
- type);
+ type,
+ fc->funding_psbt);
if (!channel) {
was_pending(command_fail(fc->cmd, LIGHTNINGD,
"Key generation failure"));
@@ -546,7 +549,8 @@ static void opening_fundee_finished(struct subd *openingd,
feerate,
local_upfront_shutdown_script,
remote_upfront_shutdown_script,
- type);
+ type,
+ NULL);
if (!channel) {
uncommitted_channel_disconnect(uc, LOG_BROKEN,
"Commit channel failed");
@@ -1090,6 +1094,8 @@ static struct command_result *json_fundchannel_complete(struct command *cmd,
if (command_check_only(cmd))
return command_check_done(cmd);
+ fc->funding_psbt = tal_steal(fc, funding_psbt);
+
/* Set the cmd to this new cmd */
peer->uncommitted_channel->fc->cmd = cmd;
msg = towire_openingd_funder_complete(NULL,
@@ -1637,7 +1643,8 @@ static struct channel *stub_chan(struct command *cmd,
NULL,
0,
&zero_channel_stats,
- tal_arr(NULL, struct channel_state_change *, 0));
+ tal_arr(NULL, struct channel_state_change *, 0),
+ NULL);
/* We don't want to gossip about this, ever. */
channel->channel_gossip = tal_free(channel->channel_gossip);
diff --git a/wallet/test/run-chain_moves_duplicate-detect.c b/wallet/test/run-chain_moves_duplicate-detect.c
index 0e518c4..2eaaa73 100644
--- a/wallet/test/run-chain_moves_duplicate-detect.c
+++ b/wallet/test/run-chain_moves_duplicate-detect.c
@@ -234,7 +234,8 @@ struct channel *new_channel(struct peer *peer UNNEEDED, u64 dbid UNNEEDED,
struct peer_update *peer_update STEALS UNNEEDED,
u64 last_stable_connection UNNEEDED,
const struct channel_stats *stats UNNEEDED,
- struct channel_state_change **state_changes STEALS UNNEEDED)
+ struct channel_state_change **state_changes STEALS UNNEEDED,
+ const struct wally_psbt *funding_psbt STEALS 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 7649e1c..26d4280 100644
--- a/wallet/test/run-db.c
+++ b/wallet/test/run-db.c
@@ -242,7 +242,8 @@ struct channel *new_channel(struct peer *peer UNNEEDED, u64 dbid UNNEEDED,
struct peer_update *peer_update STEALS UNNEEDED,
u64 last_stable_connection UNNEEDED,
const struct channel_stats *stats UNNEEDED,
- struct channel_state_change **state_changes STEALS UNNEEDED)
+ struct channel_state_change **state_changes STEALS UNNEEDED,
+ const struct wally_psbt *funding_psbt STEALS 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 189152c..9a93d2c 100644
--- a/wallet/test/run-migrate_remove_chain_moves_duplicates.c
+++ b/wallet/test/run-migrate_remove_chain_moves_duplicates.c
@@ -249,7 +249,8 @@ struct channel *new_channel(struct peer *peer UNNEEDED, u64 dbid UNNEEDED,
struct peer_update *peer_update STEALS UNNEEDED,
u64 last_stable_connection UNNEEDED,
const struct channel_stats *stats UNNEEDED,
- struct channel_state_change **state_changes STEALS UNNEEDED)
+ struct channel_state_change **state_changes STEALS UNNEEDED,
+ const struct wally_psbt *funding_psbt STEALS 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 a28c375..86ae310 100644
--- a/wallet/test/run-wallet.c
+++ b/wallet/test/run-wallet.c
@@ -1614,7 +1614,8 @@ static bool test_channel_inflight_crud(struct lightningd *ld, const tal_t *ctx)
NULL,
0,
stats,
- tal_arr(NULL, struct channel_state_change *, 0));
+ tal_arr(NULL, struct channel_state_change *, 0),
+ NULL);
db_begin_transaction(w->db);
CHECK(!wallet_err);
wallet_channel_insert(w, chan);
diff --git a/wallet/wallet.c b/wallet/wallet.c
index 07ffadd..5c561f8 100644
--- a/wallet/wallet.c
+++ b/wallet/wallet.c
@@ -1906,6 +1906,7 @@ static struct channel *wallet_stmt2channel(struct wallet *w, struct db_stmt *stm
struct peer_update *remote_update;
struct channel_stats stats;
struct channel_state_change **state_changes;
+ struct wally_psbt *funding_psbt;
peer_dbid = db_col_u64(stmt, "peer_id");
peer = find_peer_by_dbid(w->ld, peer_dbid);
@@ -2110,6 +2111,11 @@ static struct channel *wallet_stmt2channel(struct wallet *w, struct db_stmt *stm
&stats.out_msatoshi_fulfilled,
AMOUNT_MSAT(0));
+ if (!db_col_is_null(stmt, "funding_psbt"))
+ funding_psbt = db_col_psbt(tmpctx, stmt, "funding_psbt");
+ else
+ funding_psbt = NULL;
+
/* Stolen by new_channel */
state_changes = wallet_state_change_get(NULL, w, db_col_u64(stmt, "id"));
chan = new_channel(peer, db_col_u64(stmt, "id"),
@@ -2177,7 +2183,8 @@ static struct channel *wallet_stmt2channel(struct wallet *w, struct db_stmt *stm
remote_update,
db_col_u64(stmt, "last_stable_connection"),
&stats,
- state_changes);
+ state_changes,
+ funding_psbt);
if (!wallet_channel_load_inflights(w, chan)) {
tal_free(chan);
@@ -2231,6 +2238,10 @@ static struct closed_channel *wallet_stmt2closed_channel(const tal_t *ctx,
cc->their_shachain = tal_dup(cc, struct shachain, &wshachain.chain);
else
cc->their_shachain = NULL;
+ if (!db_col_is_null(stmt, "funding_psbt"))
+ cc->funding_psbt = db_col_psbt(cc, stmt, "funding_psbt");
+ else
+ cc->funding_psbt = NULL;
return cc;
}
@@ -2266,6 +2277,7 @@ void wallet_load_closed_channels(struct wallet *w,
", lease_commit_sig"
", last_stable_connection"
", shachain_remote_id"
+ ", funding_psbt"
" FROM channels"
" LEFT JOIN peers p ON p.id = peer_id"
" WHERE state = ?;"));
@@ -2311,6 +2323,7 @@ void wallet_load_one_closed_channel(struct wallet *w,
", lease_commit_sig"
", last_stable_connection"
", shachain_remote_id"
+ ", funding_psbt"
" FROM channels"
" LEFT JOIN peers p ON p.id = peer_id"
" WHERE channels.id = ?;"));
@@ -2430,6 +2443,7 @@ static bool wallet_channels_load_active(struct wallet *w)
", out_msatoshi_offered"
", out_msatoshi_fulfilled"
", close_attempt_height"
+ ", funding_psbt"
" FROM channels"
" WHERE state != ?;")); //? 0
db_bind_int(stmt, CLOSED);
@@ -2681,7 +2695,8 @@ void wallet_channel_save(struct wallet *w, struct channel *chan)
" remote_htlc_maximum_msat=?,"
" last_stable_connection=?,"
" require_confirm_inputs_remote=?,"
- " close_attempt_height=?"
+ " close_attempt_height=?,"
+ " funding_psbt=?"
" WHERE id=?"));
db_bind_u64(stmt, chan->their_shachain.id);
if (chan->scid)
@@ -2779,6 +2794,10 @@ void wallet_channel_save(struct wallet *w, struct channel *chan)
db_bind_int(stmt, chan->req_confirmed_ins[REMOTE]);
db_bind_int(stmt, chan->close_attempt_height);
+ if (chan->funding_psbt)
+ db_bind_psbt(stmt, chan->funding_psbt);
+ else
+ db_bind_null(stmt);
db_bind_u64(stmt, chan->dbid);
db_exec_prepared_v2(take(stmt));
Why this scored 20/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.