Skip to content

Commit a1a744d

Browse files
committed
lnwallet: detect and handle noop HTLCs
We update the lightning channel state machine in some key areas. If the noop TLV is set in the update_add_htlc custom records then we change the entry type to noop. When settling the HTLC if the type is noop we credit the satoshi amount back to the sender.
1 parent 8572688 commit a1a744d

File tree

3 files changed

+131
-19
lines changed

3 files changed

+131
-19
lines changed

lnwallet/aux_signer.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,20 @@ import (
1010
"github.com/lightningnetwork/lnd/tlv"
1111
)
1212

13-
// htlcCustomSigType is the TLV type that is used to encode the custom HTLC
14-
// signatures within the custom data for an existing HTLC.
15-
var htlcCustomSigType tlv.TlvType65543
13+
var (
14+
// htlcCustomSigType is the TLV type that is used to encode the custom
15+
// HTLC signatures within the custom data for an existing HTLC.
16+
htlcCustomSigType tlv.TlvType65543
17+
18+
// NoOpHtlcType is the TLV that that's used in the update_add_htlc
19+
// message to indicate the presence of a noop HTLC. This has no encoded
20+
// value, but is used to indicate that the HTLC is a noop.
21+
NoOpHtlcType tlv.TlvType65544
22+
)
23+
24+
// NoopAddHtlcType is the (golang) type of the TLV record that's used to signal
25+
// that an HTLC should be a noop HTLC.
26+
type NoopAddHtlcType = tlv.TlvType65544
1627

1728
// AuxHtlcView is a struct that contains a safe copy of an HTLC view that can
1829
// be used by aux components.

lnwallet/channel.go

Lines changed: 112 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,12 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight,
551551
remoteOutputIndex = htlc.OutputIndex
552552
}
553553

554+
customRecords := htlc.CustomRecords.Copy()
555+
556+
entryType := entryTypeForHtlc(
557+
htlc.CustomRecords, lc.channelState.ChanType,
558+
)
559+
554560
// With the scripts reconstructed (depending on if this is our commit
555561
// vs theirs or a pending commit for the remote party), we can now
556562
// re-create the original payment descriptor.
@@ -559,7 +565,7 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight,
559565
RHash: htlc.RHash,
560566
Timeout: htlc.RefundTimeout,
561567
Amount: htlc.Amt,
562-
EntryType: Add,
568+
EntryType: entryType,
563569
HtlcIndex: htlc.HtlcIndex,
564570
LogIndex: htlc.LogIndex,
565571
OnionBlob: htlc.OnionBlob,
@@ -570,7 +576,7 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight,
570576
theirPkScript: theirP2WSH,
571577
theirWitnessScript: theirWitnessScript,
572578
BlindingPoint: htlc.BlindingPoint,
573-
CustomRecords: htlc.CustomRecords.Copy(),
579+
CustomRecords: customRecords,
574580
}, nil
575581
}
576582

@@ -1100,6 +1106,10 @@ func (lc *LightningChannel) logUpdateToPayDesc(logUpdate *channeldb.LogUpdate,
11001106
},
11011107
}
11021108

1109+
pd.EntryType = entryTypeForHtlc(
1110+
pd.CustomRecords, lc.channelState.ChanType,
1111+
)
1112+
11031113
isDustRemote := HtlcIsDust(
11041114
lc.channelState.ChanType, false, lntypes.Remote,
11051115
feeRate, wireMsg.Amount.ToSatoshis(), remoteDustLimit,
@@ -1336,6 +1346,10 @@ func (lc *LightningChannel) remoteLogUpdateToPayDesc(logUpdate *channeldb.LogUpd
13361346
},
13371347
}
13381348

1349+
pd.EntryType = entryTypeForHtlc(
1350+
pd.CustomRecords, lc.channelState.ChanType,
1351+
)
1352+
13391353
// We don't need to generate an htlc script yet. This will be
13401354
// done once we sign our remote commitment.
13411355

@@ -1736,7 +1750,7 @@ func (lc *LightningChannel) restorePendingRemoteUpdates(
17361750
// but this Add restoration was a no-op as every single one of
17371751
// these Adds was already restored since they're all incoming
17381752
// htlcs on the local commitment.
1739-
if payDesc.EntryType == Add {
1753+
if payDesc.isAdd() {
17401754
continue
17411755
}
17421756

@@ -1881,7 +1895,7 @@ func (lc *LightningChannel) restorePendingLocalUpdates(
18811895
}
18821896

18831897
switch payDesc.EntryType {
1884-
case Add:
1898+
case Add, NoOpAdd:
18851899
// The HtlcIndex of the added HTLC _must_ be equal to
18861900
// the log's htlcCounter at this point. If it is not we
18871901
// panic to catch this.
@@ -2993,6 +3007,19 @@ func (lc *LightningChannel) evaluateHTLCView(view *HtlcView,
29933007
)
29943008
if rmvHeight == 0 {
29953009
switch {
3010+
// If this a noop add, then when we settle the
3011+
// HTLC, we actually credit the sender with the
3012+
// amount again, thus making it a noop. Noop
3013+
// HTLCs are only triggered by external software
3014+
// using the AuxComponents and only for channels
3015+
// that use the custom tapscript root.
3016+
case entry.EntryType == Settle &&
3017+
addEntry.EntryType == NoOpAdd:
3018+
3019+
lc.evaluateNoOpHtlc(
3020+
entry, party, &balanceDeltas,
3021+
)
3022+
29963023
// If an incoming HTLC is being settled, then
29973024
// this means that the preimage has been
29983025
// received by the settling party Therefore, we
@@ -3030,7 +3057,7 @@ func (lc *LightningChannel) evaluateHTLCView(view *HtlcView,
30303057
liveAdds := fn.Filter(
30313058
view.Updates.GetForParty(party),
30323059
func(pd *paymentDescriptor) bool {
3033-
isAdd := pd.EntryType == Add
3060+
isAdd := pd.isAdd()
30343061
shouldSkip := skip.GetForParty(party).
30353062
Contains(pd.HtlcIndex)
30363063

@@ -3069,7 +3096,7 @@ func (lc *LightningChannel) evaluateHTLCView(view *HtlcView,
30693096
// corresponding to whoseCommitmentChain.
30703097
isUncommitted := func(update *paymentDescriptor) bool {
30713098
switch update.EntryType {
3072-
case Add:
3099+
case Add, NoOpAdd:
30733100
return update.addCommitHeights.GetForParty(
30743101
whoseCommitChain,
30753102
) == 0
@@ -3145,6 +3172,55 @@ func (lc *LightningChannel) fetchParent(entry *paymentDescriptor,
31453172
return addEntry, nil
31463173
}
31473174

3175+
// balanceAboveReserve checks if the balance for the provided party is above the
3176+
// configured reserve. It also uses the balance delta for the party, to account
3177+
// for entry amounts that have been processed already.
3178+
func (lc *LightningChannel) balanceAboveReserve(party lntypes.ChannelParty,
3179+
delta btcutil.Amount, channel *channeldb.OpenChannel) bool {
3180+
3181+
channel.RLock()
3182+
defer channel.RUnlock()
3183+
3184+
c := channel
3185+
3186+
switch {
3187+
case party.IsLocal():
3188+
return c.LocalCommitment.LocalBalance.ToSatoshis()+delta >
3189+
c.LocalChanCfg.ChanReserve
3190+
3191+
case party.IsRemote():
3192+
return c.RemoteCommitment.RemoteBalance.ToSatoshis()+delta >
3193+
c.RemoteChanCfg.ChanReserve
3194+
}
3195+
3196+
return false
3197+
}
3198+
3199+
// evaluateNoOpHtlc applies the balance delta based on whether the NoOp HTLC is
3200+
// considered effective. This depends on whether the receiver is already above
3201+
// the channel reserve.
3202+
func (lc *LightningChannel) evaluateNoOpHtlc(entry *paymentDescriptor,
3203+
party lntypes.ChannelParty, balanceDeltas *lntypes.Dual[int64]) {
3204+
3205+
channel := lc.channelState
3206+
delta := btcutil.Amount(balanceDeltas.GetForParty(party))
3207+
3208+
// If the receiver has existing balance above dust then we go ahead with
3209+
// crediting the amount back to the sender. Otherwise we give the amount
3210+
// to the receiver. We do this because the receiver needs some above
3211+
// dust balance to anchor the AuxBlob. We also pass in the so-far
3212+
// calculated delta for the party, as that's effectively part of their
3213+
// balance within this view computation.
3214+
if lc.balanceAboveReserve(party, delta, channel) {
3215+
party = party.CounterParty()
3216+
}
3217+
3218+
d := int64(entry.Amount)
3219+
balanceDeltas.ModifyForParty(party, func(acc int64) int64 {
3220+
return acc + d
3221+
})
3222+
}
3223+
31483224
// generateRemoteHtlcSigJobs generates a series of HTLC signature jobs for the
31493225
// sig pool, along with a channel that if closed, will cancel any jobs after
31503226
// they have been submitted to the sigPool. This method is to be used when
@@ -3833,7 +3909,7 @@ func (lc *LightningChannel) validateCommitmentSanity(theirLogCounter,
38333909
// Go through all updates, checking that they don't violate the
38343910
// channel constraints.
38353911
for _, entry := range updates {
3836-
if entry.EntryType == Add {
3912+
if entry.isAdd() {
38373913
// An HTLC is being added, this will add to the
38383914
// number and amount in flight.
38393915
amtInFlight += entry.Amount
@@ -5712,7 +5788,7 @@ func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.RevokeAndAck) (
57125788
// don't re-forward any already processed HTLC's after a
57135789
// restart.
57145790
switch {
5715-
case pd.EntryType == Add && committedAdd && shouldFwdAdd:
5791+
case pd.isAdd() && committedAdd && shouldFwdAdd:
57165792
// Construct a reference specifying the location that
57175793
// this forwarded Add will be written in the forwarding
57185794
// package constructed at this remote height.
@@ -5731,7 +5807,7 @@ func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.RevokeAndAck) (
57315807
addUpdatesToForward, pd.toLogUpdate(),
57325808
)
57335809

5734-
case pd.EntryType != Add && committedRmv && shouldFwdRmv:
5810+
case !pd.isAdd() && committedRmv && shouldFwdRmv:
57355811
// Construct a reference specifying the location that
57365812
// this forwarded Settle/Fail will be written in the
57375813
// forwarding package constructed at this remote height.
@@ -5970,7 +6046,7 @@ func (lc *LightningChannel) GetDustSum(whoseCommit lntypes.ChannelParty,
59706046
// Grab all of our HTLCs and evaluate against the dust limit.
59716047
for e := lc.updateLogs.Local.Front(); e != nil; e = e.Next() {
59726048
pd := e.Value
5973-
if pd.EntryType != Add {
6049+
if !pd.isAdd() {
59746050
continue
59756051
}
59766052

@@ -5989,7 +6065,7 @@ func (lc *LightningChannel) GetDustSum(whoseCommit lntypes.ChannelParty,
59896065
// Grab all of their HTLCs and evaluate against the dust limit.
59906066
for e := lc.updateLogs.Remote.Front(); e != nil; e = e.Next() {
59916067
pd := e.Value
5992-
if pd.EntryType != Add {
6068+
if !pd.isAdd() {
59936069
continue
59946070
}
59956071

@@ -6062,9 +6138,12 @@ func (lc *LightningChannel) MayAddOutgoingHtlc(amt lnwire.MilliSatoshi) error {
60626138
func (lc *LightningChannel) htlcAddDescriptor(htlc *lnwire.UpdateAddHTLC,
60636139
openKey *models.CircuitKey) *paymentDescriptor {
60646140

6141+
customRecords := htlc.CustomRecords.Copy()
6142+
entryType := entryTypeForHtlc(customRecords, lc.channelState.ChanType)
6143+
60656144
return &paymentDescriptor{
60666145
ChanID: htlc.ChanID,
6067-
EntryType: Add,
6146+
EntryType: entryType,
60686147
RHash: PaymentHash(htlc.PaymentHash),
60696148
Timeout: htlc.Expiry,
60706149
Amount: htlc.Amount,
@@ -6073,7 +6152,7 @@ func (lc *LightningChannel) htlcAddDescriptor(htlc *lnwire.UpdateAddHTLC,
60736152
OnionBlob: htlc.OnionBlob,
60746153
OpenCircuitKey: openKey,
60756154
BlindingPoint: htlc.BlindingPoint,
6076-
CustomRecords: htlc.CustomRecords.Copy(),
6155+
CustomRecords: customRecords,
60776156
}
60786157
}
60796158

@@ -6126,17 +6205,20 @@ func (lc *LightningChannel) ReceiveHTLC(htlc *lnwire.UpdateAddHTLC) (uint64,
61266205
lc.updateLogs.Remote.htlcCounter)
61276206
}
61286207

6208+
customRecords := htlc.CustomRecords.Copy()
6209+
entryType := entryTypeForHtlc(customRecords, lc.channelState.ChanType)
6210+
61296211
pd := &paymentDescriptor{
61306212
ChanID: htlc.ChanID,
6131-
EntryType: Add,
6213+
EntryType: entryType,
61326214
RHash: PaymentHash(htlc.PaymentHash),
61336215
Timeout: htlc.Expiry,
61346216
Amount: htlc.Amount,
61356217
LogIndex: lc.updateLogs.Remote.logIndex,
61366218
HtlcIndex: lc.updateLogs.Remote.htlcCounter,
61376219
OnionBlob: htlc.OnionBlob,
61386220
BlindingPoint: htlc.BlindingPoint,
6139-
CustomRecords: htlc.CustomRecords.Copy(),
6221+
CustomRecords: customRecords,
61406222
}
61416223

61426224
localACKedIndex := lc.commitChains.Remote.tail().messageIndices.Local
@@ -9825,7 +9907,7 @@ func (lc *LightningChannel) unsignedLocalUpdates(remoteMessageIndex,
98259907

98269908
// We don't save add updates as they are restored from the
98279909
// remote commitment in restoreStateLogs.
9828-
if pd.EntryType == Add {
9910+
if pd.isAdd() {
98299911
continue
98309912
}
98319913

@@ -9999,3 +10081,17 @@ func (lc *LightningChannel) ZeroConfRealScid() fn.Option[lnwire.ShortChannelID]
999910081

1000010082
return fn.None[lnwire.ShortChannelID]()
1000110083
}
10084+
10085+
// entryTypeForHtlc returns the add type that should be used for adding this
10086+
// HTLC to the channel. If the channel has a tapscript root and the HTLC carries
10087+
// the NoOp bit in the custom records then we'll convert this to a NoOp add.
10088+
func entryTypeForHtlc(records lnwire.CustomRecords,
10089+
chanType channeldb.ChannelType) updateType {
10090+
10091+
noopTLV := uint64(NoOpHtlcType.TypeVal())
10092+
if _, ok := records[noopTLV]; ok && chanType.HasTapscriptRoot() {
10093+
return NoOpAdd
10094+
}
10095+
10096+
return Add
10097+
}

lnwallet/payment_descriptor.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,3 +319,8 @@ func (pd *paymentDescriptor) setCommitHeight(
319319
)
320320
}
321321
}
322+
323+
// isAdd returns true if the paymentDescriptor is of type Add.
324+
func (pd *paymentDescriptor) isAdd() bool {
325+
return pd.EntryType == Add || pd.EntryType == NoOpAdd
326+
}

0 commit comments

Comments
 (0)