Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/release-notes/release-notes-0.21.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@

# Bug Fixes

* [Fixed a lock order inversion](https://github.com/lightningnetwork/lnd/pull/11008)
between `PsbtFundingVerify` and `handleFundingCancelRequest` in the wallet.
With PSBT or batch funding the two could deadlock the wallet's single
`requestHandler` goroutine, which permanently disabled all channel funding for
the whole node: no new channel could be opened, and channels whose funding
transaction confirmed stayed in the `channelReadySent` opening state forever,
never added to the graph and never announced. Only a restart recovered.

* [Fixed several bugs](https://github.com/lightningnetwork/lnd/pull/10948)
in onion message decoding where messages that should have been rejected
per BOLT 4 were instead accepted, or a valid TLV was dropped.
Expand Down Expand Up @@ -87,3 +95,4 @@

* bitromortac
* Jared Tobin
* LNBiG
51 changes: 32 additions & 19 deletions lnwallet/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,12 @@ type LightningWallet struct {
// as key in the fundingLimbo map. Used to easily look up a channel
// reservation given a pending channel ID.
reservationIDs map[[32]byte]uint64
limboMtx sync.RWMutex

// limboMtx guards fundingLimbo and reservationIDs.
//
// NOTE: When both limboMtx and intentMtx are needed, limboMtx MUST be

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: could we make the invariant slightly more precise? “When both mutexes are needed” can be read as forbidding a safe sequential intentMtx.Lock(); intentMtx.Unlock(); limboMtx.Lock() pattern. The actual rule is that the reverse order must not be nested. Perhaps: Never acquire limboMtx while holding intentMtx. If both mutexes must be held simultaneously, acquire limboMtx before intentMtx.

// acquired first. See the note on PsbtFundingVerify.
limboMtx sync.RWMutex

// lockedOutPoints is a set of the currently locked outpoint. This
// information is kept in order to provide an easy way to unlock all
Expand Down Expand Up @@ -746,9 +751,34 @@ func (l *LightningWallet) RegisterFundingIntent(expectedID [32]byte,
// PsbtFundingVerify looks up a previously registered funding intent by its
// pending channel ID and tries to advance the state machine by verifying the
// passed PSBT.
//
// NOTE: limboMtx MUST be acquired before intentMtx, never the other way round.
// handleFundingCancelRequest holds limboMtx for the duration of the call and
// acquires intentMtx inside it, so acquiring the two in the opposite order here
// deadlocks the wallet's requestHandler goroutine, which in turn wedges all
// channel funding for the whole node.
func (l *LightningWallet) PsbtFundingVerify(pendingChanID [32]byte,
packet *psbt.Packet, skipFinalize bool) error {

// Get the channel reservation that corresponds to this pending channel
// ID. This has to happen before intentMtx is acquired, see the note
// above.
l.limboMtx.Lock()
pid, ok := l.reservationIDs[pendingChanID]
if !ok {
l.limboMtx.Unlock()
return fmt.Errorf("no channel reservation found for "+
"pendingChannelID(%x)", pendingChanID[:])
}

pendingReservation, ok := l.fundingLimbo[pid]
l.limboMtx.Unlock()

if !ok {
return fmt.Errorf("no channel reservation found for "+
"reservation ID %v", pid)
}

l.intentMtx.Lock()
defer l.intentMtx.Unlock()

Expand All @@ -772,28 +802,11 @@ func (l *LightningWallet) PsbtFundingVerify(pendingChanID [32]byte,
return fmt.Errorf("error verifying PSBT: %w", err)
}

// Get the channel reservation for that corresponds to this pending
// channel ID.
l.limboMtx.Lock()
pid, ok := l.reservationIDs[pendingChanID]
if !ok {
l.limboMtx.Unlock()
return fmt.Errorf("no channel reservation found for "+
"pendingChannelID(%x)", pendingChanID[:])
}

pendingReservation, ok := l.fundingLimbo[pid]
l.limboMtx.Unlock()

if !ok {
return fmt.Errorf("no channel reservation found for "+
"reservation ID %v", pid)
}

// Now the PSBT has been populated and verified, we can again check
// whether the value reserved for anchor fee bumping is respected.
isPublic := pendingReservation.partialState.ChannelFlags&lnwire.FFAnnounceChannel != 0
hasAnchors := pendingReservation.partialState.ChanType.HasAnchors()

return l.enforceNewReservedValue(intent, isPublic, hasAnchors)
}

Expand Down
Loading