What changed, and why it matters
This commit adds a final safety check for a new type of Lightning payment channel feature. Before an HTLC (a pending payment) is locked into the channel state, the code now asks an external 'traffic shaper' whether the channel has enough bandwidth. This closes a race condition where multiple payments could be approved based on outdated balance information. It is a defensive hardening change, not a fix for a known active exploit, and it only applies when the optional aux traffic shaper is in use.
Treat as a defensive hardening commit. Review whether the validator is also needed in other channel instantiation paths (e.g., funding flow, channel restoration) and confirm that PaymentBandwidth implementations cannot return misleading bandwidth values. No urgent patch action is indicated by the diff alone.
Security signals we found
Adds final-state validation before HTLC commitment
Closes race condition between concurrent HTLC approvals and stale bandwidth
Only active when optional AuxTrafficShaper is configured
Reuses existing PaymentBandwidth logic for consistency
No direct diff evidence of prior exploit or CVE
Evidence from the diff
The patch wires an AuxHtlcValidator into lnwallet.LightningChannel creation in peer/brontide.go for both loaded and newly added channels. The validator calls AuxTrafficShaper.ShouldHandleTraffic and, if true, PaymentBandwidth with the current commitment blob, link bandwidth, proposed amount, HTLC view, and peer vertex. If the requested amount exceeds the returned bandwidth, the HTLC is rejected. This prevents stale-balance races in custom/aux channel types that rely on an external traffic shaper.
Changed components
peer/brontide.golnwallet.LightningChannel channel optionshtlcswitch.AuxTrafficShaper integrationCustom/aux channel HTLC validation pathInspect captured patch +110 / −0
diff --git a/peer/brontide.go b/peer/brontide.go
index 0f4d6e3..1aebdfb 100644
--- a/peer/brontide.go
+++ b/peer/brontide.go
@@ -52,6 +52,7 @@ import (
"github.com/lightningnetwork/lnd/pool"
"github.com/lightningnetwork/lnd/protofsm"
"github.com/lightningnetwork/lnd/queue"
+ "github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/subscribe"
"github.com/lightningnetwork/lnd/ticker"
"github.com/lightningnetwork/lnd/tlv"
@@ -1146,6 +1147,16 @@ func (p *Brontide) loadActiveChannels(chans []*channeldb.OpenChannel) (
},
)
+ p.cfg.AuxTrafficShaper.WhenSome(
+ func(ts htlcswitch.AuxTrafficShaper) {
+ val := p.createHtlcValidator(dbChan, ts)
+ chanOpts = append(
+ chanOpts,
+ lnwallet.WithAuxHtlcValidator(val),
+ )
+ },
+ )
+
lnChan, err := lnwallet.NewLightningChannel(
p.cfg.Signer, dbChan, p.cfg.SigPool, chanOpts...,
)
@@ -5259,6 +5270,15 @@ func (p *Brontide) addActiveChannel(c *lnpeer.NewChannel) error {
chanOpts = append(chanOpts, lnwallet.WithAuxResolver(s))
})
+ p.cfg.AuxTrafficShaper.WhenSome(
+ func(ts htlcswitch.AuxTrafficShaper) {
+ val := p.createHtlcValidator(c.OpenChannel, ts)
+ chanOpts = append(
+ chanOpts, lnwallet.WithAuxHtlcValidator(val),
+ )
+ },
+ )
+
// If not already active, we'll add this channel to the set of active
// channels, so we can look it up later easily according to its channel
// ID.
@@ -5465,6 +5485,96 @@ func (p *Brontide) scaleTimeout(timeout time.Duration) time.Duration {
return timeout
}
+// auxHtlcValidator implements lnwallet.AuxHtlcValidator by checking HTLC
+// bandwidth against the traffic shaper.
+type auxHtlcValidator struct {
+ peer *Brontide
+ dbChan *channeldb.OpenChannel
+ ts htlcswitch.AuxTrafficShaper
+}
+
+// ValidateHtlc performs final aux balance validation before an HTLC is added
+// to the channel state. It calls into the traffic shaper's PaymentBandwidth
+// method to check external balance against the most up-to-date channel state,
+// preventing race conditions where multiple HTLCs could be approved based on
+// stale bandwidth.
+func (v *auxHtlcValidator) ValidateHtlc(amount,
+ linkBandwidth lnwire.MilliSatoshi,
+ customRecords lnwire.CustomRecords,
+ view lnwallet.AuxHtlcView) error {
+
+ // Get the short channel ID for logging.
+ scid := v.dbChan.ShortChannelID
+
+ // Extract the HTLC custom records to pass to the traffic shaper.
+ var htlcBlob fn.Option[tlv.Blob]
+ if len(customRecords) > 0 {
+ blob, err := customRecords.Serialize()
+ if err != nil {
+ return fmt.Errorf("unable to serialize "+
+ "custom records: %w", err)
+ }
+ htlcBlob = fn.Some(blob)
+ }
+
+ // Get the funding and commitment blobs for this channel.
+ fundingBlob := v.dbChan.CustomBlob
+ commitmentBlob := v.dbChan.LocalCommitment.CustomBlob
+
+ // Check if this channel should be handled by the traffic shaper. If
+ // not, we skip the aux validation entirely and allow the HTLC to
+ // proceed through normal validation.
+ shouldHandle, err := v.ts.ShouldHandleTraffic(
+ scid, fundingBlob, htlcBlob,
+ )
+ if err != nil {
+ return fmt.Errorf("traffic shaper failed to decide "+
+ "whether to handle traffic: %w", err)
+ }
+ if !shouldHandle {
+ return nil
+ }
+
+ peer := route.NewVertex(v.peer.IdentityKey())
+
+ // Call the traffic shaper's PaymentBandwidth method with the current
+ // state. This performs the same bandwidth checks as during
+ // pathfinding/forwarding, but against the absolute latest channel
+ // state.
+ //
+ // The linkBandwidth is provided by the channel and represents the
+ // current available balance, which is used by the traffic shaper to
+ // ensure we don't dip below channel reserves.
+ bandwidth, err := v.ts.PaymentBandwidth(
+ fundingBlob, htlcBlob, commitmentBlob,
+ linkBandwidth, amount, view, peer,
+ )
+ if err != nil {
+ return fmt.Errorf("traffic shaper bandwidth check "+
+ "failed: %w", err)
+ }
+
+ if amount > bandwidth {
+ return fmt.Errorf("insufficient aux bandwidth: "+
+ "need %v, have %v (scid=%v)", amount,
+ bandwidth, scid)
+ }
+
+ return nil
+}
+
+// createHtlcValidator creates an HTLC validator that performs final aux balance
+// validation before HTLCs are added to the channel state.
+func (p *Brontide) createHtlcValidator(dbChan *channeldb.OpenChannel,
+ ts htlcswitch.AuxTrafficShaper) lnwallet.AuxHtlcValidator {
+
+ return &auxHtlcValidator{
+ peer: p,
+ dbChan: dbChan,
+ ts: ts,
+ }
+}
+
// CoopCloseUpdates is a struct used to communicate updates for an active close
// to the caller.
type CoopCloseUpdates struct {
Why this scored 57/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.