Skip to content

Commit cb3a3d4

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 e6c703c commit cb3a3d4

File tree

3 files changed

+121
-19
lines changed

3 files changed

+121
-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: 102 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,13 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight,
551551
remoteOutputIndex = htlc.OutputIndex
552552
}
553553

554+
customRecords := htlc.CustomRecords.Copy()
555+
entryType := Add
556+
557+
if shouldSetNoop(customRecords, lc.channelState.ChanType) {
558+
entryType = NoopAdd
559+
}
560+
554561
// With the scripts reconstructed (depending on if this is our commit
555562
// vs theirs or a pending commit for the remote party), we can now
556563
// re-create the original payment descriptor.
@@ -559,7 +566,7 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight,
559566
RHash: htlc.RHash,
560567
Timeout: htlc.RefundTimeout,
561568
Amount: htlc.Amt,
562-
EntryType: Add,
569+
EntryType: entryType,
563570
HtlcIndex: htlc.HtlcIndex,
564571
LogIndex: htlc.LogIndex,
565572
OnionBlob: htlc.OnionBlob,
@@ -570,7 +577,7 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight,
570577
theirPkScript: theirP2WSH,
571578
theirWitnessScript: theirWitnessScript,
572579
BlindingPoint: htlc.BlindingPoint,
573-
CustomRecords: htlc.CustomRecords.Copy(),
580+
CustomRecords: customRecords,
574581
}, nil
575582
}
576583

@@ -1100,6 +1107,10 @@ func (lc *LightningChannel) logUpdateToPayDesc(logUpdate *channeldb.LogUpdate,
11001107
},
11011108
}
11021109

1110+
if shouldSetNoop(pd.CustomRecords, lc.channelState.ChanType) {
1111+
pd.EntryType = NoopAdd
1112+
}
1113+
11031114
isDustRemote := HtlcIsDust(
11041115
lc.channelState.ChanType, false, lntypes.Remote,
11051116
feeRate, wireMsg.Amount.ToSatoshis(), remoteDustLimit,
@@ -1336,6 +1347,10 @@ func (lc *LightningChannel) remoteLogUpdateToPayDesc(logUpdate *channeldb.LogUpd
13361347
},
13371348
}
13381349

1350+
if shouldSetNoop(pd.CustomRecords, lc.channelState.ChanType) {
1351+
pd.EntryType = NoopAdd
1352+
}
1353+
13391354
// We don't need to generate an htlc script yet. This will be
13401355
// done once we sign our remote commitment.
13411356

@@ -1736,7 +1751,7 @@ func (lc *LightningChannel) restorePendingRemoteUpdates(
17361751
// but this Add restoration was a no-op as every single one of
17371752
// these Adds was already restored since they're all incoming
17381753
// htlcs on the local commitment.
1739-
if payDesc.EntryType == Add {
1754+
if payDesc.isAdd() {
17401755
continue
17411756
}
17421757

@@ -1881,7 +1896,7 @@ func (lc *LightningChannel) restorePendingLocalUpdates(
18811896
}
18821897

18831898
switch payDesc.EntryType {
1884-
case Add:
1899+
case Add, NoopAdd:
18851900
// The HtlcIndex of the added HTLC _must_ be equal to
18861901
// the log's htlcCounter at this point. If it is not we
18871902
// panic to catch this.
@@ -2993,6 +3008,50 @@ func (lc *LightningChannel) evaluateHTLCView(view *HtlcView,
29933008
)
29943009
if rmvHeight == 0 {
29953010
switch {
3011+
// If this a noop add, then when we settle the
3012+
// HTLC, we actually credit the sender with the
3013+
// amount again, thus making it a noop. Noop
3014+
// HTLCs are only triggered by external software
3015+
// using the AuxComponents and only for channels
3016+
// that use the custom tapscript root.
3017+
case entry.EntryType == Settle &&
3018+
addEntry.EntryType == NoopAdd:
3019+
3020+
channel := lc.channelState
3021+
party := party
3022+
3023+
delta := btcutil.Amount(
3024+
balanceDeltas.GetForParty(
3025+
party,
3026+
),
3027+
)
3028+
3029+
// If the receiver has existing balance
3030+
// above dust then we go ahead with
3031+
// crediting the amount back to the
3032+
// sender. Otherwise we give the amount
3033+
// to the receiver. We do this because
3034+
// the receiver needs some above-dust
3035+
// balance to anchor the AuxBlob. We
3036+
// also pass in the so-far calculated
3037+
// delta for the party, as that's
3038+
// effectively part of their balance
3039+
// within this view computation.
3040+
if channel.BalanceAboveReserve(
3041+
party, delta,
3042+
) {
3043+
3044+
party = party.CounterParty()
3045+
}
3046+
3047+
d := int64(entry.Amount)
3048+
balanceDeltas.ModifyForParty(
3049+
party,
3050+
func(acc int64) int64 {
3051+
return acc + d
3052+
},
3053+
)
3054+
29963055
// If an incoming HTLC is being settled, then
29973056
// this means that the preimage has been
29983057
// received by the settling party Therefore, we
@@ -3030,7 +3089,7 @@ func (lc *LightningChannel) evaluateHTLCView(view *HtlcView,
30303089
liveAdds := fn.Filter(
30313090
view.Updates.GetForParty(party),
30323091
func(pd *paymentDescriptor) bool {
3033-
isAdd := pd.EntryType == Add
3092+
isAdd := pd.isAdd()
30343093
shouldSkip := skip.GetForParty(party).
30353094
Contains(pd.HtlcIndex)
30363095

@@ -3069,7 +3128,7 @@ func (lc *LightningChannel) evaluateHTLCView(view *HtlcView,
30693128
// corresponding to whoseCommitmentChain.
30703129
isUncommitted := func(update *paymentDescriptor) bool {
30713130
switch update.EntryType {
3072-
case Add:
3131+
case Add, NoopAdd:
30733132
return update.addCommitHeights.GetForParty(
30743133
whoseCommitChain,
30753134
) == 0
@@ -3833,7 +3892,7 @@ func (lc *LightningChannel) validateCommitmentSanity(theirLogCounter,
38333892
// Go through all updates, checking that they don't violate the
38343893
// channel constraints.
38353894
for _, entry := range updates {
3836-
if entry.EntryType == Add {
3895+
if entry.isAdd() {
38373896
// An HTLC is being added, this will add to the
38383897
// number and amount in flight.
38393898
amtInFlight += entry.Amount
@@ -5712,7 +5771,7 @@ func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.RevokeAndAck) (
57125771
// don't re-forward any already processed HTLC's after a
57135772
// restart.
57145773
switch {
5715-
case pd.EntryType == Add && committedAdd && shouldFwdAdd:
5774+
case pd.isAdd() && committedAdd && shouldFwdAdd:
57165775
// Construct a reference specifying the location that
57175776
// this forwarded Add will be written in the forwarding
57185777
// package constructed at this remote height.
@@ -5731,7 +5790,7 @@ func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.RevokeAndAck) (
57315790
addUpdatesToForward, pd.toLogUpdate(),
57325791
)
57335792

5734-
case pd.EntryType != Add && committedRmv && shouldFwdRmv:
5793+
case !pd.isAdd() && committedRmv && shouldFwdRmv:
57355794
// Construct a reference specifying the location that
57365795
// this forwarded Settle/Fail will be written in the
57375796
// forwarding package constructed at this remote height.
@@ -5970,7 +6029,7 @@ func (lc *LightningChannel) GetDustSum(whoseCommit lntypes.ChannelParty,
59706029
// Grab all of our HTLCs and evaluate against the dust limit.
59716030
for e := lc.updateLogs.Local.Front(); e != nil; e = e.Next() {
59726031
pd := e.Value
5973-
if pd.EntryType != Add {
6032+
if !pd.isAdd() {
59746033
continue
59756034
}
59766035

@@ -5989,7 +6048,7 @@ func (lc *LightningChannel) GetDustSum(whoseCommit lntypes.ChannelParty,
59896048
// Grab all of their HTLCs and evaluate against the dust limit.
59906049
for e := lc.updateLogs.Remote.Front(); e != nil; e = e.Next() {
59916050
pd := e.Value
5992-
if pd.EntryType != Add {
6051+
if !pd.isAdd() {
59936052
continue
59946053
}
59956054

@@ -6062,9 +6121,15 @@ func (lc *LightningChannel) MayAddOutgoingHtlc(amt lnwire.MilliSatoshi) error {
60626121
func (lc *LightningChannel) htlcAddDescriptor(htlc *lnwire.UpdateAddHTLC,
60636122
openKey *models.CircuitKey) *paymentDescriptor {
60646123

6124+
entryType := Add
6125+
customRecords := htlc.CustomRecords.Copy()
6126+
if shouldSetNoop(customRecords, lc.channelState.ChanType) {
6127+
entryType = NoopAdd
6128+
}
6129+
60656130
return &paymentDescriptor{
60666131
ChanID: htlc.ChanID,
6067-
EntryType: Add,
6132+
EntryType: entryType,
60686133
RHash: PaymentHash(htlc.PaymentHash),
60696134
Timeout: htlc.Expiry,
60706135
Amount: htlc.Amount,
@@ -6073,7 +6138,7 @@ func (lc *LightningChannel) htlcAddDescriptor(htlc *lnwire.UpdateAddHTLC,
60736138
OnionBlob: htlc.OnionBlob,
60746139
OpenCircuitKey: openKey,
60756140
BlindingPoint: htlc.BlindingPoint,
6076-
CustomRecords: htlc.CustomRecords.Copy(),
6141+
CustomRecords: customRecords,
60776142
}
60786143
}
60796144

@@ -6126,17 +6191,23 @@ func (lc *LightningChannel) ReceiveHTLC(htlc *lnwire.UpdateAddHTLC) (uint64,
61266191
lc.updateLogs.Remote.htlcCounter)
61276192
}
61286193

6194+
entryType := Add
6195+
customRecords := htlc.CustomRecords.Copy()
6196+
if shouldSetNoop(customRecords, lc.channelState.ChanType) {
6197+
entryType = NoopAdd
6198+
}
6199+
61296200
pd := &paymentDescriptor{
61306201
ChanID: htlc.ChanID,
6131-
EntryType: Add,
6202+
EntryType: entryType,
61326203
RHash: PaymentHash(htlc.PaymentHash),
61336204
Timeout: htlc.Expiry,
61346205
Amount: htlc.Amount,
61356206
LogIndex: lc.updateLogs.Remote.logIndex,
61366207
HtlcIndex: lc.updateLogs.Remote.htlcCounter,
61376208
OnionBlob: htlc.OnionBlob,
61386209
BlindingPoint: htlc.BlindingPoint,
6139-
CustomRecords: htlc.CustomRecords.Copy(),
6210+
CustomRecords: customRecords,
61406211
}
61416212

61426213
localACKedIndex := lc.commitChains.Remote.tail().messageIndices.Local
@@ -9825,7 +9896,7 @@ func (lc *LightningChannel) unsignedLocalUpdates(remoteMessageIndex,
98259896

98269897
// We don't save add updates as they are restored from the
98279898
// remote commitment in restoreStateLogs.
9828-
if pd.EntryType == Add {
9899+
if pd.isAdd() {
98299900
continue
98309901
}
98319902

@@ -9999,3 +10070,18 @@ func (lc *LightningChannel) ZeroConfRealScid() fn.Option[lnwire.ShortChannelID]
999910070

1000010071
return fn.None[lnwire.ShortChannelID]()
1000110072
}
10073+
10074+
// shouldSetNoop checks the custom records of the entry and the channel type and
10075+
// returns a boolean indicating whether this add entry should be converted to a
10076+
// noop add type. This will only return true if the TLV field signalling the use
10077+
// of a noop HTLC is set, and the channel has a custom tapscript root.
10078+
func shouldSetNoop(records lnwire.CustomRecords,
10079+
chanType channeldb.ChannelType) bool {
10080+
10081+
noopTLV := uint64(NoopHtlcType.TypeVal())
10082+
if _, ok := records[noopTLV]; ok && chanType.HasTapscriptRoot() {
10083+
return true
10084+
}
10085+
10086+
return false
10087+
}

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)