funding: process channel_ready messages inline in the coordinator
What changed, and why it matters
This commit refactors how LND handles a specific channel-funding message (channel_ready). Previously, every such message launched a new goroutine; now most are processed inline in the main coordinator loop, with goroutines only used when actually waiting for a local funding confirmation signal or doing heavy finalization work. The change is described by the authors as a performance and consistency improvement, not a security fix. There is no direct evidence in the commit or supplied references that this patch addresses a known vulnerability or attack.
Treat as a normal code-quality/performance refactor. Review for race conditions introduced by moving the barrier and signal logic into the inline path, and verify that duplicate channel_ready messages cannot bypass the barrier under the new synchronous dispatch. No immediate security response is indicated by the available evidence.
Security signals we found
Refactor of concurrency model for channel_ready handling
Barrier map (handleChannelReadyBarriers) retained to prevent duplicate processing
Inline FindChannel lookup may reduce resource-exhaustion surface from duplicate messages
No explicit security bug or CVE mentioned in commit or supplied references
Evidence from the diff
The patch changes funding/manager.go so that reservationCoordinator() calls f.handleChannelReady(peer, msg) synchronously instead of spawning a goroutine. handleChannelReady is split into a lightweight inline entry point and processChannelReady (the heavy body). The inline path checks localDiscoverySignals and either: (1) spawns a goroutine to wait for the signal then calls processChannelReady, (2) does a quick FindChannel lookup inline and short-circuits duplicates/established channels, or (3) spawns a goroutine for the remaining finalization work. This reduces goroutine churn and makes message handling consistent with other funding message types. The commit message frames it as a refactor/performance improvement.
Changed components
funding/manager.goreservationCoordinator message loophandleChannelReady / processChannelReady functionslocalDiscoverySignals and handleChannelReadyBarriers synchronizationInspect captured patch +116 / −48
diff --git a/funding/manager.go b/funding/manager.go
index f64c303..417ad9c 100644
--- a/funding/manager.go
+++ b/funding/manager.go
@@ -1060,8 +1060,7 @@ func (f *Manager) reservationCoordinator() {
f.funderProcessFundingSigned(fmsg.peer, msg)
case *lnwire.ChannelReady:
- f.wg.Add(1)
- go f.handleChannelReady(fmsg.peer, msg)
+ f.handleChannelReady(fmsg.peer, msg)
case *lnwire.Warning:
f.handleWarningMsg(fmsg.peer, msg)
@@ -3653,7 +3652,7 @@ func (f *Manager) receivedChannelReady(node *btcec.PublicKey,
}
// Finally, the barrier signal is removed once we finish
- // `handleChannelReady`. If we can still find the signal, we haven't
+ // `processChannelReady`. If we can still find the signal, we haven't
// finished processing it yet.
_, loaded := f.handleChannelReadyBarriers.Load(chanID)
@@ -4051,11 +4050,9 @@ func genFirstStateMusigNonce(channel *channeldb.OpenChannel,
// handleChannelReady finalizes the channel funding process and enables the
// channel to enter normal operating mode.
-func (f *Manager) handleChannelReady(peer lnpeer.Peer, //nolint:funlen
+func (f *Manager) handleChannelReady(peer lnpeer.Peer,
msg *lnwire.ChannelReady) {
- defer f.wg.Done()
-
// Notify the aux hook that the specified peer just established a
// channel with us, identified by the given channel ID.
f.cfg.AuxChannelNegotiator.WhenSome(
@@ -4064,68 +4061,139 @@ func (f *Manager) handleChannelReady(peer lnpeer.Peer, //nolint:funlen
},
)
- // If we are in development mode, we'll wait for specified duration
- // before processing the channel ready message.
- if f.cfg.Dev != nil {
- duration := f.cfg.Dev.ProcessChannelReadyWait
- log.Warnf("Channel(%v): sleeping %v before processing "+
- "channel_ready", msg.ChanID, duration)
-
- select {
- case <-time.After(duration):
- log.Warnf("Channel(%v): slept %v before processing "+
- "channel_ready", msg.ChanID, duration)
- case <-f.quit:
- log.Warnf("Channel(%v): quit sleeping", msg.ChanID)
- return
- }
- }
-
log.Debugf("Received ChannelReady for ChannelID(%v) from "+
"peer %x", msg.ChanID,
peer.IdentityKey().SerializeCompressed())
- // We now load or create a new channel barrier for this channel.
+ // We now load or create a new channel barrier for this channel. If
+ // we are currently in the process of handling a channel_ready message
+ // for this channel, ignore the duplicate.
_, loaded := f.handleChannelReadyBarriers.LoadOrStore(
msg.ChanID, struct{}{},
)
-
- // If we are currently in the process of handling a channel_ready
- // message for this channel, ignore.
if loaded {
log.Infof("Already handling channelReady for "+
"ChannelID(%v), ignoring.", msg.ChanID)
return
}
- // If not already handling channelReady for this channel, then the
- // `LoadOrStore` has set up a barrier, and it will be removed once this
- // function exits.
- defer f.handleChannelReadyBarriers.Delete(msg.ChanID)
-
+ // Check whether we need to wait for the local funding confirmation flow
+ // to finish before we can proceed with this message. The
+ // localDiscoverySignal is only present for channels that we are
+ // actively funding and is bounded by the maximum number of pending
+ // channels.
localDiscoverySignal, ok := f.localDiscoverySignals.Load(msg.ChanID)
if ok {
- // Before we proceed with processing the channel_ready
- // message, we'll wait for the local waitForFundingConfirmation
- // goroutine to signal that it has the necessary state in
- // place. Otherwise, we may be missing critical information
- // required to handle forwarded HTLC's.
+ f.wg.Add(1)
+ go func() {
+ defer f.wg.Done()
+ defer f.handleChannelReadyBarriers.Delete(
+ msg.ChanID,
+ )
+
+ // Wait for the local waitForFundingConfirmation
+ // goroutine to signal that it has the necessary state
+ // in place. Otherwise, we may be missing critical
+ // information required to handle forwarded HTLC's.
+ select {
+ case <-localDiscoverySignal:
+ case <-f.quit:
+ return
+ }
+
+ f.localDiscoverySignals.Delete(msg.ChanID)
+ f.processChannelReady(peer, msg)
+ }()
+
+ return
+ }
+
+ // No signal wait needed. Perform a lightweight channel lookup inline
+ // to short-circuit bogus or already-established channels without
+ // blocking the coordinator on heavier processing.
+ chanID := msg.ChanID
+ channel, err := f.cfg.FindChannel(peer.IdentityKey(), chanID)
+ if err != nil {
+ f.handleChannelReadyBarriers.Delete(msg.ChanID)
+
+ log.Errorf("Unable to locate ChannelID(%v), cannot "+
+ "complete funding", chanID)
+
+ return
+ }
+
+ // If the RemoteNextRevocation is non-nil, then the channel has
+ // already been fully established and we've processed channel_ready
+ // for it at least once. We short-circuit inline to avoid redoing the
+ // heavy work in processChannelReady (DB writes, nonce generation,
+ // AddNewChannel) on every duplicate channel_ready the peer sends.
+ // Note that the happy path where the channel is actively being
+ // funded goes through the localDiscoverySignal branch above.
+ if channel.RemoteNextRevocation != nil {
+ // Even though we're ignoring the rest of the message, we
+ // still need to refresh the peer's alias if they negotiated
+ // the option_scid_alias feature and sent a (possibly updated)
+ // AliasScid. The peer may resend channel_ready to rotate or
+ // update their alias for invoice route hints.
+ if channel.NegotiatedAliasFeature() && msg.AliasScid != nil {
+ err := f.cfg.AliasManager.PutPeerAlias(
+ chanID, *msg.AliasScid,
+ )
+ if err != nil {
+ log.Errorf("unable to store peer's alias: "+
+ "%v", err)
+ }
+ }
+
+ f.handleChannelReadyBarriers.Delete(msg.ChanID)
+
+ log.Infof("Received duplicate channelReady for "+
+ "ChannelID(%v), ignoring.", chanID)
+
+ return
+ }
+
+ // Channel exists and hasn't been fully established yet — this is a
+ // legitimate first channel_ready. Dispatch the remaining work (DB
+ // writes, nonce generation, AddNewChannel) in a goroutine to avoid
+ // blocking the coordinator.
+ f.wg.Add(1)
+ go func() {
+ defer f.wg.Done()
+ defer f.handleChannelReadyBarriers.Delete(msg.ChanID)
+
+ f.processChannelReady(peer, msg)
+ }()
+}
+
+// processChannelReady completes the channel_ready handling after any required
+// signal waits. It looks up the channel in the database and finalizes the
+// funding flow by inserting the remote party's next revocation point and
+// handing the channel off to the peer for normal operation.
+func (f *Manager) processChannelReady(peer lnpeer.Peer,
+ msg *lnwire.ChannelReady) {
+
+ // If we are in development mode, we'll wait for specified duration
+ // before processing the channel ready message.
+ if f.cfg.Dev != nil {
+ duration := f.cfg.Dev.ProcessChannelReadyWait
+ log.Warnf("Channel(%v): sleeping %v before processing "+
+ "channel_ready", msg.ChanID, duration)
+
select {
- case <-localDiscoverySignal:
- // Fallthrough
+ case <-time.After(duration):
+ log.Warnf("Channel(%v): slept %v before processing "+
+ "channel_ready", msg.ChanID, duration)
case <-f.quit:
+ log.Warnf("Channel(%v): quit sleeping", msg.ChanID)
return
}
-
- // With the signal received, we can now safely delete the entry
- // from the map.
- f.localDiscoverySignals.Delete(msg.ChanID)
}
- // First, we'll attempt to locate the channel whose funding workflow is
- // being finalized by this message. We go to the database rather than
- // our reservation map as we may have restarted, mid funding flow. Also
- // provide the node's public key to make the search faster.
+ // We'll attempt to locate the channel whose funding workflow is being
+ // finalized by this message. We go to the database rather than our
+ // reservation map as we may have restarted mid funding flow. The
+ // node's public key is provided to scope the search.
chanID := msg.ChanID
channel, err := f.cfg.FindChannel(peer.IdentityKey(), chanID)
if err != nil {
@@ -4150,7 +4218,7 @@ func (f *Manager) handleChannelReady(peer lnpeer.Peer, //nolint:funlen
// during invoice creation. In the zero-conf case, it is also used to
// provide a ChannelUpdate to the remote peer. This is done before the
// call to InsertNextRevocation in case the call to PutPeerAlias fails.
- // If it were to fail on the first call to handleChannelReady, we
+ // If it were to fail on the first call to processChannelReady, we
// wouldn't want the channel to be usable yet.
if channel.NegotiatedAliasFeature() {
// If the AliasScid field is nil, we must fail out. We will
Why this scored 24/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.