Skip to content

Commit 32b42bc

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 4f00673 commit 32b42bc

File tree

3 files changed

+132
-19
lines changed

3 files changed

+132
-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: 113 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,56 @@ 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,
3179+
delta lnwire.MilliSatoshi, 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+
return c.LocalCommitment.LocalBalance+delta > localReserve
3192+
3193+
case party.IsRemote():
3194+
return c.RemoteCommitment.RemoteBalance+delta > remoteReserve
3195+
}
3196+
3197+
return false
3198+
}
3199+
3200+
// evaluateNoOpHtlc applies the balance delta based on whether the NoOp HTLC is
3201+
// considered effective. This depends on whether the receiver is already above
3202+
// the channel reserve.
3203+
func (lc *LightningChannel) evaluateNoOpHtlc(entry *paymentDescriptor,
3204+
party lntypes.ChannelParty, balanceDeltas *lntypes.Dual[int64]) {
3205+
3206+
channel := lc.channelState
3207+
delta := lnwire.MilliSatoshi(balanceDeltas.GetForParty(party))
3208+
3209+
// If the receiver has existing balance above dust then we go ahead with
3210+
// crediting the amount back to the sender. Otherwise we give the amount
3211+
// to the receiver. We do this because the receiver needs some above
3212+
// reserve balance to anchor the AuxBlob. We also pass in the so-far
3213+
// calculated delta for the party, as that's effectively part of their
3214+
// balance within this view computation.
3215+
if balanceAboveReserve(party, delta, channel) {
3216+
party = party.CounterParty()
3217+
}
3218+
3219+
d := int64(entry.Amount)
3220+
balanceDeltas.ModifyForParty(party, func(acc int64) int64 {
3221+
return acc + d
3222+
})
3223+
}
3224+
31483225
// generateRemoteHtlcSigJobs generates a series of HTLC signature jobs for the
31493226
// sig pool, along with a channel that if closed, will cancel any jobs after
31503227
// they have been submitted to the sigPool. This method is to be used when
@@ -3833,7 +3910,7 @@ func (lc *LightningChannel) validateCommitmentSanity(theirLogCounter,
38333910
// Go through all updates, checking that they don't violate the
38343911
// channel constraints.
38353912
for _, entry := range updates {
3836-
if entry.EntryType == Add {
3913+
if entry.isAdd() {
38373914
// An HTLC is being added, this will add to the
38383915
// number and amount in flight.
38393916
amtInFlight += entry.Amount
@@ -5712,7 +5789,7 @@ func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.RevokeAndAck) (
57125789
// don't re-forward any already processed HTLC's after a
57135790
// restart.
57145791
switch {
5715-
case pd.EntryType == Add && committedAdd && shouldFwdAdd:
5792+
case pd.isAdd() && committedAdd && shouldFwdAdd:
57165793
// Construct a reference specifying the location that
57175794
// this forwarded Add will be written in the forwarding
57185795
// package constructed at this remote height.
@@ -5731,7 +5808,7 @@ func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.RevokeAndAck) (
57315808
addUpdatesToForward, pd.toLogUpdate(),
57325809
)
57335810

5734-
case pd.EntryType != Add && committedRmv && shouldFwdRmv:
5811+
case !pd.isAdd() && committedRmv && shouldFwdRmv:
57355812
// Construct a reference specifying the location that
57365813
// this forwarded Settle/Fail will be written in the
57375814
// forwarding package constructed at this remote height.
@@ -5970,7 +6047,7 @@ func (lc *LightningChannel) GetDustSum(whoseCommit lntypes.ChannelParty,
59706047
// Grab all of our HTLCs and evaluate against the dust limit.
59716048
for e := lc.updateLogs.Local.Front(); e != nil; e = e.Next() {
59726049
pd := e.Value
5973-
if pd.EntryType != Add {
6050+
if !pd.isAdd() {
59746051
continue
59756052
}
59766053

@@ -5989,7 +6066,7 @@ func (lc *LightningChannel) GetDustSum(whoseCommit lntypes.ChannelParty,
59896066
// Grab all of their HTLCs and evaluate against the dust limit.
59906067
for e := lc.updateLogs.Remote.Front(); e != nil; e = e.Next() {
59916068
pd := e.Value
5992-
if pd.EntryType != Add {
6069+
if !pd.isAdd() {
59936070
continue
59946071
}
59956072

@@ -6062,9 +6139,12 @@ func (lc *LightningChannel) MayAddOutgoingHtlc(amt lnwire.MilliSatoshi) error {
60626139
func (lc *LightningChannel) htlcAddDescriptor(htlc *lnwire.UpdateAddHTLC,
60636140
openKey *models.CircuitKey) *paymentDescriptor {
60646141

6142+
customRecords := htlc.CustomRecords.Copy()
6143+
entryType := entryTypeForHtlc(customRecords, lc.channelState.ChanType)
6144+
60656145
return &paymentDescriptor{
60666146
ChanID: htlc.ChanID,
6067-
EntryType: Add,
6147+
EntryType: entryType,
60686148
RHash: PaymentHash(htlc.PaymentHash),
60696149
Timeout: htlc.Expiry,
60706150
Amount: htlc.Amount,
@@ -6073,7 +6153,7 @@ func (lc *LightningChannel) htlcAddDescriptor(htlc *lnwire.UpdateAddHTLC,
60736153
OnionBlob: htlc.OnionBlob,
60746154
OpenCircuitKey: openKey,
60756155
BlindingPoint: htlc.BlindingPoint,
6076-
CustomRecords: htlc.CustomRecords.Copy(),
6156+
CustomRecords: customRecords,
60776157
}
60786158
}
60796159

@@ -6126,17 +6206,20 @@ func (lc *LightningChannel) ReceiveHTLC(htlc *lnwire.UpdateAddHTLC) (uint64,
61266206
lc.updateLogs.Remote.htlcCounter)
61276207
}
61286208

6209+
customRecords := htlc.CustomRecords.Copy()
6210+
entryType := entryTypeForHtlc(customRecords, lc.channelState.ChanType)
6211+
61296212
pd := &paymentDescriptor{
61306213
ChanID: htlc.ChanID,
6131-
EntryType: Add,
6214+
EntryType: entryType,
61326215
RHash: PaymentHash(htlc.PaymentHash),
61336216
Timeout: htlc.Expiry,
61346217
Amount: htlc.Amount,
61356218
LogIndex: lc.updateLogs.Remote.logIndex,
61366219
HtlcIndex: lc.updateLogs.Remote.htlcCounter,
61376220
OnionBlob: htlc.OnionBlob,
61386221
BlindingPoint: htlc.BlindingPoint,
6139-
CustomRecords: htlc.CustomRecords.Copy(),
6222+
CustomRecords: customRecords,
61406223
}
61416224

61426225
localACKedIndex := lc.commitChains.Remote.tail().messageIndices.Local
@@ -9825,7 +9908,7 @@ func (lc *LightningChannel) unsignedLocalUpdates(remoteMessageIndex,
98259908

98269909
// We don't save add updates as they are restored from the
98279910
// remote commitment in restoreStateLogs.
9828-
if pd.EntryType == Add {
9911+
if pd.isAdd() {
98299912
continue
98309913
}
98319914

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

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

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)