Skip to content

Commit 445ab1b

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 03cb342 commit 445ab1b

File tree

3 files changed

+146
-19
lines changed

3 files changed

+146
-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, its presence 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: 127 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+
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,70 @@ 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 balanceAboveReserve(party lntypes.ChannelParty, delta int64,
3179+
channel *channeldb.OpenChannel) bool {
3180+
3181+
channel.RLock()
3182+
defer channel.RUnlock()
3183+
3184+
c := channel
3185+
3186+
localReserve := lnwire.NewMSatFromSatoshis(c.LocalChanCfg.ChanReserve)
3187+
remoteReserve := lnwire.NewMSatFromSatoshis(c.RemoteChanCfg.ChanReserve)
3188+
3189+
switch {
3190+
case party.IsLocal():
3191+
totalLocal := c.LocalCommitment.LocalBalance
3192+
if delta >= 0 {
3193+
totalLocal += lnwire.MilliSatoshi(delta)
3194+
} else {
3195+
totalLocal -= lnwire.MilliSatoshi(-1 * delta)
3196+
}
3197+
3198+
return totalLocal > localReserve
3199+
3200+
case party.IsRemote():
3201+
totalRemote := c.RemoteCommitment.RemoteBalance
3202+
if delta >= 0 {
3203+
totalRemote += lnwire.MilliSatoshi(delta)
3204+
} else {
3205+
totalRemote -= lnwire.MilliSatoshi(-1 * delta)
3206+
}
3207+
3208+
return totalRemote > remoteReserve
3209+
}
3210+
3211+
return false
3212+
}
3213+
3214+
// evaluateNoOpHtlc applies the balance delta based on whether the NoOp HTLC is
3215+
// considered effective. This depends on whether the receiver is already above
3216+
// the channel reserve.
3217+
func (lc *LightningChannel) evaluateNoOpHtlc(entry *paymentDescriptor,
3218+
party lntypes.ChannelParty, balanceDeltas *lntypes.Dual[int64]) {
3219+
3220+
channel := lc.channelState
3221+
delta := balanceDeltas.GetForParty(party)
3222+
3223+
// If the receiver has existing balance above reserve then we go ahead
3224+
// with crediting the amount back to the sender. Otherwise we give the
3225+
// amount to the receiver. We do this because the receiver needs some
3226+
// above reserve balance to anchor the AuxBlob. We also pass in the so
3227+
// far calculated delta for the party, as that's effectively part of
3228+
// their balance within this view computation.
3229+
if balanceAboveReserve(party, delta, channel) {
3230+
party = party.CounterParty()
3231+
}
3232+
3233+
d := int64(entry.Amount)
3234+
balanceDeltas.ModifyForParty(party, func(acc int64) int64 {
3235+
return acc + d
3236+
})
3237+
}
3238+
31483239
// generateRemoteHtlcSigJobs generates a series of HTLC signature jobs for the
31493240
// sig pool, along with a channel that if closed, will cancel any jobs after
31503241
// they have been submitted to the sigPool. This method is to be used when
@@ -3833,7 +3924,7 @@ func (lc *LightningChannel) validateCommitmentSanity(theirLogCounter,
38333924
// Go through all updates, checking that they don't violate the
38343925
// channel constraints.
38353926
for _, entry := range updates {
3836-
if entry.EntryType == Add {
3927+
if entry.isAdd() {
38373928
// An HTLC is being added, this will add to the
38383929
// number and amount in flight.
38393930
amtInFlight += entry.Amount
@@ -5712,7 +5803,7 @@ func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.RevokeAndAck) (
57125803
// don't re-forward any already processed HTLC's after a
57135804
// restart.
57145805
switch {
5715-
case pd.EntryType == Add && committedAdd && shouldFwdAdd:
5806+
case pd.isAdd() && committedAdd && shouldFwdAdd:
57165807
// Construct a reference specifying the location that
57175808
// this forwarded Add will be written in the forwarding
57185809
// package constructed at this remote height.
@@ -5731,7 +5822,7 @@ func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.RevokeAndAck) (
57315822
addUpdatesToForward, pd.toLogUpdate(),
57325823
)
57335824

5734-
case pd.EntryType != Add && committedRmv && shouldFwdRmv:
5825+
case !pd.isAdd() && committedRmv && shouldFwdRmv:
57355826
// Construct a reference specifying the location that
57365827
// this forwarded Settle/Fail will be written in the
57375828
// forwarding package constructed at this remote height.
@@ -5970,7 +6061,7 @@ func (lc *LightningChannel) GetDustSum(whoseCommit lntypes.ChannelParty,
59706061
// Grab all of our HTLCs and evaluate against the dust limit.
59716062
for e := lc.updateLogs.Local.Front(); e != nil; e = e.Next() {
59726063
pd := e.Value
5973-
if pd.EntryType != Add {
6064+
if !pd.isAdd() {
59746065
continue
59756066
}
59766067

@@ -5989,7 +6080,7 @@ func (lc *LightningChannel) GetDustSum(whoseCommit lntypes.ChannelParty,
59896080
// Grab all of their HTLCs and evaluate against the dust limit.
59906081
for e := lc.updateLogs.Remote.Front(); e != nil; e = e.Next() {
59916082
pd := e.Value
5992-
if pd.EntryType != Add {
6083+
if !pd.isAdd() {
59936084
continue
59946085
}
59956086

@@ -6062,9 +6153,12 @@ func (lc *LightningChannel) MayAddOutgoingHtlc(amt lnwire.MilliSatoshi) error {
60626153
func (lc *LightningChannel) htlcAddDescriptor(htlc *lnwire.UpdateAddHTLC,
60636154
openKey *models.CircuitKey) *paymentDescriptor {
60646155

6156+
customRecords := htlc.CustomRecords.Copy()
6157+
entryType := entryTypeForHtlc(customRecords, lc.channelState.ChanType)
6158+
60656159
return &paymentDescriptor{
60666160
ChanID: htlc.ChanID,
6067-
EntryType: Add,
6161+
EntryType: entryType,
60686162
RHash: PaymentHash(htlc.PaymentHash),
60696163
Timeout: htlc.Expiry,
60706164
Amount: htlc.Amount,
@@ -6073,7 +6167,7 @@ func (lc *LightningChannel) htlcAddDescriptor(htlc *lnwire.UpdateAddHTLC,
60736167
OnionBlob: htlc.OnionBlob,
60746168
OpenCircuitKey: openKey,
60756169
BlindingPoint: htlc.BlindingPoint,
6076-
CustomRecords: htlc.CustomRecords.Copy(),
6170+
CustomRecords: customRecords,
60776171
}
60786172
}
60796173

@@ -6126,17 +6220,20 @@ func (lc *LightningChannel) ReceiveHTLC(htlc *lnwire.UpdateAddHTLC) (uint64,
61266220
lc.updateLogs.Remote.htlcCounter)
61276221
}
61286222

6223+
customRecords := htlc.CustomRecords.Copy()
6224+
entryType := entryTypeForHtlc(customRecords, lc.channelState.ChanType)
6225+
61296226
pd := &paymentDescriptor{
61306227
ChanID: htlc.ChanID,
6131-
EntryType: Add,
6228+
EntryType: entryType,
61326229
RHash: PaymentHash(htlc.PaymentHash),
61336230
Timeout: htlc.Expiry,
61346231
Amount: htlc.Amount,
61356232
LogIndex: lc.updateLogs.Remote.logIndex,
61366233
HtlcIndex: lc.updateLogs.Remote.htlcCounter,
61376234
OnionBlob: htlc.OnionBlob,
61386235
BlindingPoint: htlc.BlindingPoint,
6139-
CustomRecords: htlc.CustomRecords.Copy(),
6236+
CustomRecords: customRecords,
61406237
}
61416238

61426239
localACKedIndex := lc.commitChains.Remote.tail().messageIndices.Local
@@ -9825,7 +9922,7 @@ func (lc *LightningChannel) unsignedLocalUpdates(remoteMessageIndex,
98259922

98269923
// We don't save add updates as they are restored from the
98279924
// remote commitment in restoreStateLogs.
9828-
if pd.EntryType == Add {
9925+
if pd.isAdd() {
98299926
continue
98309927
}
98319928

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

1000010097
return fn.None[lnwire.ShortChannelID]()
1000110098
}
10099+
10100+
// entryTypeForHtlc returns the add type that should be used for adding this
10101+
// HTLC to the channel. If the channel has a tapscript root and the HTLC carries
10102+
// the NoOp bit in the custom records then we'll convert this to a NoOp add.
10103+
func entryTypeForHtlc(records lnwire.CustomRecords,
10104+
chanType channeldb.ChannelType) updateType {
10105+
10106+
noopTLV := uint64(NoOpHtlcType.TypeVal())
10107+
if _, ok := records[noopTLV]; ok && chanType.HasTapscriptRoot() {
10108+
return NoOpAdd
10109+
}
10110+
10111+
return Add
10112+
}

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)