funding: fix race in itest for zero-conf funding
What changed, and why it matters
This commit fixes a timing bug in the LND Lightning node software that affected zero-confirmation channels. When a channel became publicly announced after six confirmations, the node would tell the network about the new channel ID before its own internal payment forwarding switch was ready to use that ID. On slow database backends, this created a brief window where payments routed through that channel could fail with an 'UnknownNextPeer' error. The fix simply reorders two internal steps so the switch is updated before the channel is announced to the network. It is a reliability fix rather than a vulnerability that allows theft or loss of funds.
Treat as a normal reliability/bug-fix patch. Users running nodes with zero-conf public channels and slow backends (e.g., Postgres) benefit most from upgrading. No emergency action is required; there is no evidence of fund loss or remote exploitation.
Security signals we found
Race condition between local switch state update and network gossip announcement
Payment forwarding failure (UnknownNextPeer) for confirmed SCID of public zero-conf channels
Timing-dependent behavior amplified by slow database backends (e.g., Postgres)
Fix is a reordering of existing operations, not a logic change
Test updated to enforce ReportShortChanID before channel announcement
Evidence from the diff
In funding/manager.go’s waitForZeroConfChannel, ReportShortChanID (which refreshes the htlcswitch link’s baseIndex so forwarding works under the confirmed SCID) was called after addToGraph (which announces the confirmed SCID via gossip). With slow backends such as Postgres, addToGraph can take long enough that peers learn the confirmed SCID and attempt to forward through it before the local switch maps that SCID to the link. The patch moves ReportShortChanID before the graph update and alias cleanup, and updates the integration test to assert this ordering. Forwards using either the alias or confirmed SCID resolve to the same link via getLinkByMapping/baseIndex.
Changed components
funding/manager.gofunding/manager_test.gohtlcswitch forwarding path for zero-conf channelsgossip announcement of confirmed SCIDInspect captured patch +26 / −21
diff --git a/funding/manager.go b/funding/manager.go
index 5711ed8..81b673f 100644
--- a/funding/manager.go
+++ b/funding/manager.go
@@ -3957,6 +3957,22 @@ func (f *Manager) waitForZeroConfChannel(c *channeldb.OpenChannel) error {
// Six confirmations have been reached. If this channel is public,
// we'll delete some of the alias mappings the gossiper uses.
+ //
+ // Tell the Switch to refresh the relevant ChannelLink so that forwards
+ // under the confirmed SCID are possible. We do this BEFORE updating the
+ // graph to avoid a race where other nodes learn about the confirmed
+ // SCID from gossip before our switch is ready to handle forwards using
+ // it. This is especially important for integration tests.
+ err = f.cfg.ReportShortChanID(c.FundingOutpoint)
+ if err != nil {
+ // This should only fail if the link is not found in the
+ // Switch's linkIndex map. If this is the case, then the peer
+ // has gone offline and the next time the link is loaded, it
+ // will have a refreshed state. Just log an error here.
+ log.Errorf("unable to report scid for zero-conf channel "+
+ "channel: %v", err)
+ }
+
isPublic := c.ChannelFlags&lnwire.FFAnnounceChannel != 0
if isPublic {
err = f.cfg.AliasManager.DeleteSixConfs(c.ShortChannelID)
@@ -3985,19 +4001,6 @@ func (f *Manager) waitForZeroConfChannel(c *channeldb.OpenChannel) error {
}
}
- // Since we have now marked down the confirmed SCID, we'll also need to
- // tell the Switch to refresh the relevant ChannelLink so that forwards
- // under the confirmed SCID are possible if this is a public channel.
- err = f.cfg.ReportShortChanID(c.FundingOutpoint)
- if err != nil {
- // This should only fail if the link is not found in the
- // Switch's linkIndex map. If this is the case, then the peer
- // has gone offline and the next time the link is loaded, it
- // will have a refreshed state. Just log an error here.
- log.Errorf("unable to report scid for zero-conf channel "+
- "channel: %v", err)
- }
-
// Update the confirmed transaction's label.
f.makeLabelForTx(c)
diff --git a/funding/manager_test.go b/funding/manager_test.go
index 45b847f..26563c8 100644
--- a/funding/manager_test.go
+++ b/funding/manager_test.go
@@ -4726,14 +4726,9 @@ func testZeroConf(t *testing.T, chanType *lnwire.ChannelType) {
assertConfirmationHeight(t, alice, chanID, 1)
assertConfirmationHeight(t, bob, chanID, 1)
- // For taproot channels, we don't expect them to be announced atm.
- if !isTaprootChanType(chanType) {
- assertChannelAnnouncements(
- t, alice, bob, fundingAmt, nil, nil, nil, nil,
- )
- }
-
- // Both Alice and Bob should send on reportScidChan.
+ // Both Alice and Bob should call ReportShortChanID first (before
+ // sending announcements) to avoid a race where other nodes learn about
+ // the confirmed SCID before the switch is ready.
select {
case <-alice.reportScidChan:
case <-time.After(time.Second * 5):
@@ -4746,6 +4741,13 @@ func testZeroConf(t *testing.T, chanType *lnwire.ChannelType) {
t.Fatalf("did not call ReportShortChanID in time")
}
+ // For taproot channels, we don't expect them to be announced atm.
+ if !isTaprootChanType(chanType) {
+ assertChannelAnnouncements(
+ t, alice, bob, fundingAmt, nil, nil, nil, nil,
+ )
+ }
+
// Send along the 6-confirmation channel so that announcement sigs can
// be exchanged.
alice.mockNotifier.sixConfChannel <- &chainntnfs.TxConfirmation{
Why this scored 37/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.