Skip to content

Commit c0a0006

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 729d049 commit c0a0006

File tree

3 files changed

+134
-19
lines changed

3 files changed

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

554+
customRecords := htlc.CustomRecords.Copy()
555+
entryType := Add
556+
557+
// If the noop HTLC TLV is set then change this HTLC's type to noop.
558+
noopTLV := uint64(NoopHtlcType.TypeVal())
559+
560+
if shouldSetNoop(customRecords, lc.channelState.ChanType) {
561+
entryType = NoopAdd
562+
}
563+
564+
// The NoopAdd HTLC is an internal construct, and isn't meant to show up
565+
// on the wire. So we'll remove the special element from the set of
566+
// custom records.
567+
delete(customRecords, noopTLV)
568+
554569
// With the scripts reconstructed (depending on if this is our commit
555570
// vs theirs or a pending commit for the remote party), we can now
556571
// re-create the original payment descriptor.
@@ -559,7 +574,7 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight,
559574
RHash: htlc.RHash,
560575
Timeout: htlc.RefundTimeout,
561576
Amount: htlc.Amt,
562-
EntryType: Add,
577+
EntryType: entryType,
563578
HtlcIndex: htlc.HtlcIndex,
564579
LogIndex: htlc.LogIndex,
565580
OnionBlob: htlc.OnionBlob,
@@ -570,7 +585,7 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight,
570585
theirPkScript: theirP2WSH,
571586
theirWitnessScript: theirWitnessScript,
572587
BlindingPoint: htlc.BlindingPoint,
573-
CustomRecords: htlc.CustomRecords.Copy(),
588+
CustomRecords: customRecords,
574589
}, nil
575590
}
576591

@@ -1100,6 +1115,10 @@ func (lc *LightningChannel) logUpdateToPayDesc(logUpdate *channeldb.LogUpdate,
11001115
},
11011116
}
11021117

1118+
if shouldSetNoop(pd.CustomRecords, lc.channelState.ChanType) {
1119+
pd.EntryType = NoopAdd
1120+
}
1121+
11031122
isDustRemote := HtlcIsDust(
11041123
lc.channelState.ChanType, false, lntypes.Remote,
11051124
feeRate, wireMsg.Amount.ToSatoshis(), remoteDustLimit,
@@ -1336,6 +1355,10 @@ func (lc *LightningChannel) remoteLogUpdateToPayDesc(logUpdate *channeldb.LogUpd
13361355
},
13371356
}
13381357

1358+
if shouldSetNoop(pd.CustomRecords, lc.channelState.ChanType) {
1359+
pd.EntryType = NoopAdd
1360+
}
1361+
13391362
// We don't need to generate an htlc script yet. This will be
13401363
// done once we sign our remote commitment.
13411364

@@ -1736,7 +1759,7 @@ func (lc *LightningChannel) restorePendingRemoteUpdates(
17361759
// but this Add restoration was a no-op as every single one of
17371760
// these Adds was already restored since they're all incoming
17381761
// htlcs on the local commitment.
1739-
if payDesc.EntryType == Add {
1762+
if payDesc.isAdd() {
17401763
continue
17411764
}
17421765

@@ -1881,7 +1904,7 @@ func (lc *LightningChannel) restorePendingLocalUpdates(
18811904
}
18821905

18831906
switch payDesc.EntryType {
1884-
case Add:
1907+
case Add, NoopAdd:
18851908
// The HtlcIndex of the added HTLC _must_ be equal to
18861909
// the log's htlcCounter at this point. If it is not we
18871910
// panic to catch this.
@@ -2993,6 +3016,47 @@ func (lc *LightningChannel) evaluateHTLCView(view *HtlcView,
29933016
)
29943017
if rmvHeight == 0 {
29953018
switch {
3019+
// If this a noop add, then when we settle the
3020+
// HTLC, we actually credit the sender with the
3021+
// amount again, thus making it a noop. Noop
3022+
// HTLCs are only triggered by external software
3023+
// using the AuxComponents and only for channels
3024+
// that use the custom tapscript root.
3025+
case entry.EntryType == Settle &&
3026+
addEntry.EntryType == NoopAdd:
3027+
3028+
channel := lc.channelState
3029+
party := party
3030+
delta := balanceDeltas.GetForParty(
3031+
party,
3032+
)
3033+
3034+
// If the receiver has existing balance
3035+
// above dust then we go ahead with
3036+
// crediting the amount back to the
3037+
// sender. Otherwise we give the amount
3038+
// to the receiver. We do this because
3039+
// the receiver needs some above-dust
3040+
// balance to anchor the AuxBlob. We
3041+
// also pass in the so-far calculated
3042+
// delta for the party, as that's
3043+
// effectively part of their balance
3044+
// within this view computation.
3045+
if channel.BalanceAboveReserve(
3046+
party, delta,
3047+
) {
3048+
3049+
party = party.CounterParty()
3050+
}
3051+
3052+
d := int64(entry.Amount)
3053+
balanceDeltas.ModifyForParty(
3054+
party,
3055+
func(acc int64) int64 {
3056+
return acc + d
3057+
},
3058+
)
3059+
29963060
// If an incoming HTLC is being settled, then
29973061
// this means that the preimage has been
29983062
// received by the settling party Therefore, we
@@ -3030,7 +3094,7 @@ func (lc *LightningChannel) evaluateHTLCView(view *HtlcView,
30303094
liveAdds := fn.Filter(
30313095
view.Updates.GetForParty(party),
30323096
func(pd *paymentDescriptor) bool {
3033-
isAdd := pd.EntryType == Add
3097+
isAdd := pd.isAdd()
30343098
shouldSkip := skip.GetForParty(party).
30353099
Contains(pd.HtlcIndex)
30363100

@@ -3069,7 +3133,7 @@ func (lc *LightningChannel) evaluateHTLCView(view *HtlcView,
30693133
// corresponding to whoseCommitmentChain.
30703134
isUncommitted := func(update *paymentDescriptor) bool {
30713135
switch update.EntryType {
3072-
case Add:
3136+
case Add, NoopAdd:
30733137
return update.addCommitHeights.GetForParty(
30743138
whoseCommitChain,
30753139
) == 0
@@ -3833,7 +3897,7 @@ func (lc *LightningChannel) validateCommitmentSanity(theirLogCounter,
38333897
// Go through all updates, checking that they don't violate the
38343898
// channel constraints.
38353899
for _, entry := range updates {
3836-
if entry.EntryType == Add {
3900+
if entry.isAdd() {
38373901
// An HTLC is being added, this will add to the
38383902
// number and amount in flight.
38393903
amtInFlight += entry.Amount
@@ -4489,6 +4553,14 @@ func (lc *LightningChannel) ProcessChanSyncMsg(ctx context.Context,
44894553
// Next, we'll need to send over any updates we sent as part of
44904554
// this new proposed commitment state.
44914555
for _, logUpdate := range commitDiff.LogUpdates {
4556+
//nolint:ll
4557+
if htlc, ok := logUpdate.UpdateMsg.(*lnwire.UpdateAddHTLC); ok {
4558+
delete(htlc.CustomRecords, uint64(NoopHtlcType.TypeVal()))
4559+
4560+
if len(htlc.CustomRecords) == 0 {
4561+
htlc.CustomRecords = nil
4562+
}
4563+
}
44924564
commitUpdates = append(
44934565
commitUpdates, logUpdate.UpdateMsg,
44944566
)
@@ -5712,7 +5784,7 @@ func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.RevokeAndAck) (
57125784
// don't re-forward any already processed HTLC's after a
57135785
// restart.
57145786
switch {
5715-
case pd.EntryType == Add && committedAdd && shouldFwdAdd:
5787+
case pd.isAdd() && committedAdd && shouldFwdAdd:
57165788
// Construct a reference specifying the location that
57175789
// this forwarded Add will be written in the forwarding
57185790
// package constructed at this remote height.
@@ -5731,7 +5803,7 @@ func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.RevokeAndAck) (
57315803
addUpdatesToForward, pd.toLogUpdate(),
57325804
)
57335805

5734-
case pd.EntryType != Add && committedRmv && shouldFwdRmv:
5806+
case !pd.isAdd() && committedRmv && shouldFwdRmv:
57355807
// Construct a reference specifying the location that
57365808
// this forwarded Settle/Fail will be written in the
57375809
// forwarding package constructed at this remote height.
@@ -5970,7 +6042,7 @@ func (lc *LightningChannel) GetDustSum(whoseCommit lntypes.ChannelParty,
59706042
// Grab all of our HTLCs and evaluate against the dust limit.
59716043
for e := lc.updateLogs.Local.Front(); e != nil; e = e.Next() {
59726044
pd := e.Value
5973-
if pd.EntryType != Add {
6045+
if !pd.isAdd() {
59746046
continue
59756047
}
59766048

@@ -5989,7 +6061,7 @@ func (lc *LightningChannel) GetDustSum(whoseCommit lntypes.ChannelParty,
59896061
// Grab all of their HTLCs and evaluate against the dust limit.
59906062
for e := lc.updateLogs.Remote.Front(); e != nil; e = e.Next() {
59916063
pd := e.Value
5992-
if pd.EntryType != Add {
6064+
if !pd.isAdd() {
59936065
continue
59946066
}
59956067

@@ -6062,9 +6134,15 @@ func (lc *LightningChannel) MayAddOutgoingHtlc(amt lnwire.MilliSatoshi) error {
60626134
func (lc *LightningChannel) htlcAddDescriptor(htlc *lnwire.UpdateAddHTLC,
60636135
openKey *models.CircuitKey) *paymentDescriptor {
60646136

6137+
entryType := Add
6138+
customRecords := htlc.CustomRecords.Copy()
6139+
if shouldSetNoop(customRecords, lc.channelState.ChanType) {
6140+
entryType = NoopAdd
6141+
}
6142+
60656143
return &paymentDescriptor{
60666144
ChanID: htlc.ChanID,
6067-
EntryType: Add,
6145+
EntryType: entryType,
60686146
RHash: PaymentHash(htlc.PaymentHash),
60696147
Timeout: htlc.Expiry,
60706148
Amount: htlc.Amount,
@@ -6073,7 +6151,7 @@ func (lc *LightningChannel) htlcAddDescriptor(htlc *lnwire.UpdateAddHTLC,
60736151
OnionBlob: htlc.OnionBlob,
60746152
OpenCircuitKey: openKey,
60756153
BlindingPoint: htlc.BlindingPoint,
6076-
CustomRecords: htlc.CustomRecords.Copy(),
6154+
CustomRecords: customRecords,
60776155
}
60786156
}
60796157

@@ -6126,17 +6204,23 @@ func (lc *LightningChannel) ReceiveHTLC(htlc *lnwire.UpdateAddHTLC) (uint64,
61266204
lc.updateLogs.Remote.htlcCounter)
61276205
}
61286206

6207+
entryType := Add
6208+
customRecords := htlc.CustomRecords.Copy()
6209+
if shouldSetNoop(customRecords, lc.channelState.ChanType) {
6210+
entryType = NoopAdd
6211+
}
6212+
61296213
pd := &paymentDescriptor{
61306214
ChanID: htlc.ChanID,
6131-
EntryType: Add,
6215+
EntryType: entryType,
61326216
RHash: PaymentHash(htlc.PaymentHash),
61336217
Timeout: htlc.Expiry,
61346218
Amount: htlc.Amount,
61356219
LogIndex: lc.updateLogs.Remote.logIndex,
61366220
HtlcIndex: lc.updateLogs.Remote.htlcCounter,
61376221
OnionBlob: htlc.OnionBlob,
61386222
BlindingPoint: htlc.BlindingPoint,
6139-
CustomRecords: htlc.CustomRecords.Copy(),
6223+
CustomRecords: customRecords,
61406224
}
61416225

61426226
localACKedIndex := lc.commitChains.Remote.tail().messageIndices.Local
@@ -9825,7 +9909,7 @@ func (lc *LightningChannel) unsignedLocalUpdates(remoteMessageIndex,
98259909

98269910
// We don't save add updates as they are restored from the
98279911
// remote commitment in restoreStateLogs.
9828-
if pd.EntryType == Add {
9912+
if pd.isAdd() {
98299913
continue
98309914
}
98319915

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

1000010084
return fn.None[lnwire.ShortChannelID]()
1000110085
}
10086+
10087+
// shouldSetNoop checks the custom records of the entry and the channel type and
10088+
// returns a boolean indicating whether this add entry should be converted to a
10089+
// noop add type. This will only return true if the TLV field signalling the use
10090+
// of a noop HTLC is set, and the channel has a custom tapscript root.
10091+
func shouldSetNoop(records lnwire.CustomRecords,
10092+
chanType channeldb.ChannelType) bool {
10093+
10094+
noopTLV := uint64(NoopHtlcType.TypeVal())
10095+
if _, ok := records[noopTLV]; ok && chanType.HasTapscriptRoot() {
10096+
return true
10097+
}
10098+
10099+
return false
10100+
}

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)