wallet: persist the inflight i_sent_sigs flag across restarts
What changed, and why it matters
This change fixes a bug where a flag tracking whether a node had already sent transaction signatures during a 'splice' (a way to resize a Lightning channel) was stored only in memory. If the node restarted mid-process, it could forget that it had already sent signatures and either resend them or get confused about the splice state. Persisting the flag to the database makes splicing more robust across restarts.
Treat as a robustness/correctness fix. Review whether the memory-only flag could have led to any exploitable state confusion (e.g., signature replay, stuck splice, or channel desynchronization) and consider backporting if splicing is enabled in production releases.
Security signals we found
State inconsistency across restarts in channel funding protocol
Memory-only flag now persisted to database
Splice protocol correctness fix
Regression test added for flag persistence
Evidence from the diff
The commit adds persistence for the i_sent_sigs flag in the channel_funding_inflights database table. Previously this flag was memory-only, which could lead to incorrect state after a restart during an in-flight splice. The wallet now binds inflight->i_sent_sigs into the UPDATE statement in wallet_inflight_save(). A regression test verifies that both sides of a splice set the flag correctly in the database.
Changed components
wallet/wallet.cchannel_funding_inflights database tableLightning splice protocol implementationInspect captured patch +31 / −0
### tests/test_splicing.py
@@ -665,6 +665,35 @@ def test_route_by_old_scid(node_factory, bitcoind):
l1.rpc.waitsendpay(inv2['payment_hash'])
+@pytest.mark.openchannel('v1')
+@pytest.mark.openchannel('v2')
+@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need')
+def test_splice_sent_sigs_flag(node_factory, bitcoind):
+ l1, l2 = node_factory.line_graph(2, fundamount=1000000,
+ wait_for_announce=True,
+ opts={'may_reconnect': True,
+ 'allow_warning': True})
+
+ chan_id = l1.get_channel_id(l2)
+
+ funds_result = l1.rpc.fundpsbt("109000sat", 0, 0, excess_as_change=True)
+
+ result = l1.rpc.splice_init(chan_id, 100000, funds_result['psbt'])
+ result = l1.rpc.splice_update(chan_id, result['psbt'])
+ assert result['commitments_secured'] is False
+ result = l1.rpc.splice_update(chan_id, result['psbt'])
+ assert result['commitments_secured'] is True
+
+ # l2 contributed nothing so it signs first. These two lines bracket that:
+ # it has sent tx_signatures and is now waiting for l1's.
+ l2.daemon.wait_for_logs([r'peer_out WIRE_TX_SIGNATURES',
+ r'Splice: Awaiting signature message'])
+ assert l2.db_query("SELECT count(*) as c FROM channel_funding_inflights;")[0]['c'] == 1
+
+ assert l1.db_query("SELECT i_sent_sigs FROM channel_funding_inflights;")[0]['i_sent_sigs'] == 0
+ assert l2.db_query("SELECT i_sent_sigs FROM channel_funding_inflights;")[0]['i_sent_sigs'] == 1
+
+
@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need')
@pytest.mark.openchannel('v1')
@pytest.mark.openchannel('v2')
### wallet/wallet.c
@@ -1583,6 +1583,7 @@ void wallet_inflight_save(struct wallet *w,
", last_tx=?"
", last_sig=?"
", locked_scid=?"
+ ", i_sent_sigs=?"
" WHERE"
" channel_id=?"
" AND funding_tx_id=?"
@@ -1600,6 +1601,7 @@ void wallet_inflight_save(struct wallet *w,
db_bind_short_channel_id(stmt, *inflight->locked_scid);
else
db_bind_null(stmt);
+ db_bind_int(stmt, inflight->i_sent_sigs);
db_bind_u64(stmt, inflight->channel->dbid);
db_bind_txid(stmt, &inflight->funding->outpoint.txid);
db_bind_int(stmt, inflight->funding->outpoint.n);Why this scored 42/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.