-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Add NoopAdd
HTLCs
#9871
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add NoopAdd
HTLCs
#9871
Changes from 1 commit
138b716
aacefb9
a5a15f6
e048669
4a34f78
47d1365
97bbbf1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -551,6 +551,12 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight, | |
remoteOutputIndex = htlc.OutputIndex | ||
} | ||
|
||
customRecords := htlc.CustomRecords.Copy() | ||
|
||
entryType := lc.entryTypeForHtlc( | ||
customRecords, lc.channelState.ChanType, | ||
) | ||
|
||
// With the scripts reconstructed (depending on if this is our commit | ||
// vs theirs or a pending commit for the remote party), we can now | ||
// re-create the original payment descriptor. | ||
|
@@ -559,7 +565,7 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight, | |
RHash: htlc.RHash, | ||
Timeout: htlc.RefundTimeout, | ||
Amount: htlc.Amt, | ||
EntryType: Add, | ||
EntryType: entryType, | ||
HtlcIndex: htlc.HtlcIndex, | ||
LogIndex: htlc.LogIndex, | ||
OnionBlob: htlc.OnionBlob, | ||
|
@@ -570,7 +576,7 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight, | |
theirPkScript: theirP2WSH, | ||
theirWitnessScript: theirWitnessScript, | ||
BlindingPoint: htlc.BlindingPoint, | ||
CustomRecords: htlc.CustomRecords.Copy(), | ||
CustomRecords: customRecords, | ||
}, nil | ||
} | ||
|
||
|
@@ -1100,6 +1106,10 @@ func (lc *LightningChannel) logUpdateToPayDesc(logUpdate *channeldb.LogUpdate, | |
}, | ||
} | ||
|
||
pd.EntryType = lc.entryTypeForHtlc( | ||
pd.CustomRecords, lc.channelState.ChanType, | ||
) | ||
|
||
isDustRemote := HtlcIsDust( | ||
lc.channelState.ChanType, false, lntypes.Remote, | ||
feeRate, wireMsg.Amount.ToSatoshis(), remoteDustLimit, | ||
|
@@ -1336,6 +1346,10 @@ func (lc *LightningChannel) remoteLogUpdateToPayDesc(logUpdate *channeldb.LogUpd | |
}, | ||
} | ||
|
||
pd.EntryType = lc.entryTypeForHtlc( | ||
pd.CustomRecords, lc.channelState.ChanType, | ||
) | ||
|
||
// We don't need to generate an htlc script yet. This will be | ||
// done once we sign our remote commitment. | ||
|
||
|
@@ -1736,7 +1750,7 @@ func (lc *LightningChannel) restorePendingRemoteUpdates( | |
// but this Add restoration was a no-op as every single one of | ||
// these Adds was already restored since they're all incoming | ||
// htlcs on the local commitment. | ||
if payDesc.EntryType == Add { | ||
if payDesc.isAdd() { | ||
continue | ||
} | ||
|
||
|
@@ -1881,7 +1895,7 @@ func (lc *LightningChannel) restorePendingLocalUpdates( | |
} | ||
|
||
switch payDesc.EntryType { | ||
case Add: | ||
case Add, NoOpAdd: | ||
// The HtlcIndex of the added HTLC _must_ be equal to | ||
// the log's htlcCounter at this point. If it is not we | ||
// panic to catch this. | ||
|
@@ -2993,6 +3007,22 @@ func (lc *LightningChannel) evaluateHTLCView(view *HtlcView, | |
) | ||
if rmvHeight == 0 { | ||
switch { | ||
// If this a noop add, then when we settle the | ||
// HTLC, we may credit the sender with the | ||
// amount again, thus making it a noop. Noop | ||
// HTLCs are only triggered by external software | ||
// using the AuxComponents and only for channels | ||
// that use the custom tapscript root. The | ||
// criteria about whether the noop will be | ||
// effective is whether the receiver is already | ||
// sitting above reserve. | ||
case entry.EntryType == Settle && | ||
GeorgeTsagk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
addEntry.EntryType == NoOpAdd: | ||
|
||
lc.evaluateNoOpHtlc( | ||
entry, party, &balanceDeltas, | ||
) | ||
|
||
// If an incoming HTLC is being settled, then | ||
// this means that the preimage has been | ||
// received by the settling party Therefore, we | ||
|
@@ -3030,7 +3060,7 @@ func (lc *LightningChannel) evaluateHTLCView(view *HtlcView, | |
liveAdds := fn.Filter( | ||
view.Updates.GetForParty(party), | ||
func(pd *paymentDescriptor) bool { | ||
isAdd := pd.EntryType == Add | ||
isAdd := pd.isAdd() | ||
shouldSkip := skip.GetForParty(party). | ||
Contains(pd.HtlcIndex) | ||
|
||
|
@@ -3069,7 +3099,7 @@ func (lc *LightningChannel) evaluateHTLCView(view *HtlcView, | |
// corresponding to whoseCommitmentChain. | ||
isUncommitted := func(update *paymentDescriptor) bool { | ||
switch update.EntryType { | ||
case Add: | ||
case Add, NoOpAdd: | ||
return update.addCommitHeights.GetForParty( | ||
whoseCommitChain, | ||
) == 0 | ||
|
@@ -3145,6 +3175,92 @@ func (lc *LightningChannel) fetchParent(entry *paymentDescriptor, | |
return addEntry, nil | ||
} | ||
|
||
// balanceAboveReserve checks if the balance for the provided party is above the | ||
// configured reserve. It also uses the balance delta for the party, to account | ||
// for entry amounts that have been processed already. | ||
func balanceAboveReserve(party lntypes.ChannelParty, delta int64, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I follow this check - since it's only called when we are processing the Settle, and before that there must be an Add. To lock in that Add, we must have already checked that the channel balance wouldn't drop below the reserve, plus a few other checks such as the fee buffer. So this method should always return true? Unless we don't check for reserves when processing tap HTLCs? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This would be for the remote side. If Alice is sending to Bob, here we want to check if Bob is below reserve. Currently if Bob is at 0% (no reserve) we can still send him 1sat even if that doesn't take him above the configured reserve. The noop-add will actually nullify the send only if Bob is sitting above reserve. This requirement is related to how tap assets work, as we need to make sure we have a sufficient amount in order to anchor off-chain metadata that needs to be present on-chain. |
||
channel *channeldb.OpenChannel) bool { | ||
|
||
// We're going to access the channel state, so let's make sure we're | ||
// holding the lock. | ||
channel.RLock() | ||
defer channel.RUnlock() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This usually means a method should exist on the struct itself. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I moved it here after addressing a recommendation in a previous comment Happy to change it again |
||
|
||
// For calculating whether a party is above reserve we are going to | ||
// use the channel state local/remote balance of the corresponding | ||
// commitment. This balance corresponds to the balance of each party | ||
// after the most recent revocation. That's the balance on top of which | ||
// we may apply the balance delta of the currently processed HTLCs. It | ||
// is important for the calculated balance to match between us and our | ||
// peer, as any disagreement over the balances here can lead to a force | ||
// closure. | ||
c := channel | ||
|
||
localReserve := lnwire.NewMSatFromSatoshis(c.LocalChanCfg.ChanReserve) | ||
remoteReserve := lnwire.NewMSatFromSatoshis(c.RemoteChanCfg.ChanReserve) | ||
|
||
switch { | ||
case party.IsLocal(): | ||
// For the local party we'll consult the local balance of the | ||
// local commitment. Then we'll correctly add the delta based on | ||
// whether it's negative or not. | ||
totalLocal := c.LocalCommitment.LocalBalance | ||
GeorgeTsagk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if delta >= 0 { | ||
totalLocal += lnwire.MilliSatoshi(delta) | ||
} else { | ||
totalLocal -= lnwire.MilliSatoshi(-1 * delta) | ||
} | ||
|
||
return totalLocal > localReserve | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Think this needs to be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it doesn't really matter if we're greater vs greater-equal than the value here, worst case we need to send an extra 1msat to pass the check What really matters here is that both peers perform the same check with the same values, as there absolutely needs to be an agreement here on whether the check is satisfied (otherwise it would lead to balance disagreements -> conflicting commitments -> force closure) Added some context around this in the code too |
||
|
||
case party.IsRemote(): | ||
// For the remote party we'll consult the remote balance of the | ||
// remote commitment. Then we'll correctly add the delta based | ||
// on whether it's negative or not. | ||
totalRemote := c.RemoteCommitment.RemoteBalance | ||
if delta >= 0 { | ||
totalRemote += lnwire.MilliSatoshi(delta) | ||
} else { | ||
totalRemote -= lnwire.MilliSatoshi(-1 * delta) | ||
} | ||
|
||
return totalRemote > remoteReserve | ||
GeorgeTsagk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
return false | ||
} | ||
|
||
// evaluateNoOpHtlc applies the balance delta based on whether the NoOp HTLC is | ||
// considered effective. This depends on whether the receiver is already above | ||
// the channel reserve. | ||
func (lc *LightningChannel) evaluateNoOpHtlc(entry *paymentDescriptor, | ||
party lntypes.ChannelParty, balanceDeltas *lntypes.Dual[int64]) { | ||
|
||
channel := lc.channelState | ||
delta := balanceDeltas.GetForParty(party) | ||
|
||
// If the receiver has existing balance above reserve then we go ahead | ||
yyforyongyu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// with crediting the amount back to the sender. Otherwise we give the | ||
// amount to the receiver. We do this because the receiver needs some | ||
// above reserve balance to anchor the AuxBlob. We also pass in the so | ||
// far calculated delta for the party, as that's effectively part of | ||
// their balance within this view computation. | ||
if balanceAboveReserve(party, delta, channel) { | ||
party = party.CounterParty() | ||
|
||
// The noop is effective, meaning that the settlement will | ||
// credit the amount back to the sender. Let's mark this as it | ||
// may be needed later when processing the settle entry, where | ||
// we won't be able to perform the above check again. | ||
entry.noOpSettle = true | ||
} | ||
|
||
d := int64(entry.Amount) | ||
balanceDeltas.ModifyForParty(party, func(acc int64) int64 { | ||
return acc + d | ||
}) | ||
} | ||
|
||
// generateRemoteHtlcSigJobs generates a series of HTLC signature jobs for the | ||
// sig pool, along with a channel that if closed, will cancel any jobs after | ||
// they have been submitted to the sigPool. This method is to be used when | ||
|
@@ -3833,7 +3949,7 @@ func (lc *LightningChannel) validateCommitmentSanity(theirLogCounter, | |
// Go through all updates, checking that they don't violate the | ||
// channel constraints. | ||
for _, entry := range updates { | ||
if entry.EntryType == Add { | ||
if entry.isAdd() { | ||
GeorgeTsagk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// An HTLC is being added, this will add to the | ||
// number and amount in flight. | ||
amtInFlight += entry.Amount | ||
|
@@ -4668,6 +4784,15 @@ func (lc *LightningChannel) computeView(view *HtlcView, | |
if whoseCommitChain == lntypes.Local && | ||
u.EntryType == Settle { | ||
|
||
// If this settle was a result of an | ||
// effective noop add entry, then we | ||
// don't need to record the amount as it | ||
// was never sent over to the other | ||
// side. | ||
if u.noOpSettle { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
continue | ||
} | ||
|
||
lc.recordSettlement(party, u.Amount) | ||
} | ||
} | ||
|
@@ -5712,7 +5837,7 @@ func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.RevokeAndAck) ( | |
// don't re-forward any already processed HTLC's after a | ||
// restart. | ||
switch { | ||
case pd.EntryType == Add && committedAdd && shouldFwdAdd: | ||
case pd.isAdd() && committedAdd && shouldFwdAdd: | ||
// Construct a reference specifying the location that | ||
// this forwarded Add will be written in the forwarding | ||
// package constructed at this remote height. | ||
|
@@ -5731,7 +5856,7 @@ func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.RevokeAndAck) ( | |
addUpdatesToForward, pd.toLogUpdate(), | ||
) | ||
|
||
case pd.EntryType != Add && committedRmv && shouldFwdRmv: | ||
case !pd.isAdd() && committedRmv && shouldFwdRmv: | ||
// Construct a reference specifying the location that | ||
// this forwarded Settle/Fail will be written in the | ||
// forwarding package constructed at this remote height. | ||
|
@@ -5970,7 +6095,7 @@ func (lc *LightningChannel) GetDustSum(whoseCommit lntypes.ChannelParty, | |
// Grab all of our HTLCs and evaluate against the dust limit. | ||
for e := lc.updateLogs.Local.Front(); e != nil; e = e.Next() { | ||
pd := e.Value | ||
if pd.EntryType != Add { | ||
if !pd.isAdd() { | ||
continue | ||
} | ||
|
||
|
@@ -5989,7 +6114,7 @@ func (lc *LightningChannel) GetDustSum(whoseCommit lntypes.ChannelParty, | |
// Grab all of their HTLCs and evaluate against the dust limit. | ||
for e := lc.updateLogs.Remote.Front(); e != nil; e = e.Next() { | ||
pd := e.Value | ||
if pd.EntryType != Add { | ||
if !pd.isAdd() { | ||
continue | ||
} | ||
|
||
|
@@ -6062,9 +6187,14 @@ func (lc *LightningChannel) MayAddOutgoingHtlc(amt lnwire.MilliSatoshi) error { | |
func (lc *LightningChannel) htlcAddDescriptor(htlc *lnwire.UpdateAddHTLC, | ||
openKey *models.CircuitKey) *paymentDescriptor { | ||
|
||
customRecords := htlc.CustomRecords.Copy() | ||
entryType := lc.entryTypeForHtlc( | ||
customRecords, lc.channelState.ChanType, | ||
) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There isn't an error path here yet, but we should return an error if this type is used for any channel type other than the aux channels. This'll serve to contain the logic a bit. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why error out instead of no-op'ing the noop? i.e if lc.ChanType().HasTapscriptRoot() {
entryType = NoopAdd
} if this for any reason is happening over a normal channel, the TLV field will be ignored and won't change any state whatsoever There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if that happens to a normal channel which does not understand the TLV, you propose just treating it as a normal Add ? But I think we should error the channel if a peer tries to trick us into adding a NOOPADD for a normal channel. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, if for any reason (maybe even by accident) the noop gets signaled then we should simply revert to old behavior.
Need to account for potential bugs in software. If for any reason the flag "escapes" the scope of being set only over custom channels we wouldn't want to cause force-closes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
To make the failure explicit, as it should never happen. If we silently downgrade an unexpected noop HTLC (say from a legacy static key channel), then it may be harder to figure out what happened after the fact. We can put this check in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If that's the case we should let the FC happen so we know something is wrong, otherwise the bug will be sitting there silently forever. |
||
return &paymentDescriptor{ | ||
ChanID: htlc.ChanID, | ||
EntryType: Add, | ||
EntryType: entryType, | ||
RHash: PaymentHash(htlc.PaymentHash), | ||
Timeout: htlc.Expiry, | ||
Amount: htlc.Amount, | ||
|
@@ -6073,7 +6203,7 @@ func (lc *LightningChannel) htlcAddDescriptor(htlc *lnwire.UpdateAddHTLC, | |
OnionBlob: htlc.OnionBlob, | ||
OpenCircuitKey: openKey, | ||
BlindingPoint: htlc.BlindingPoint, | ||
CustomRecords: htlc.CustomRecords.Copy(), | ||
CustomRecords: customRecords, | ||
} | ||
} | ||
|
||
|
@@ -6126,17 +6256,22 @@ func (lc *LightningChannel) ReceiveHTLC(htlc *lnwire.UpdateAddHTLC) (uint64, | |
lc.updateLogs.Remote.htlcCounter) | ||
} | ||
|
||
customRecords := htlc.CustomRecords.Copy() | ||
entryType := lc.entryTypeForHtlc( | ||
customRecords, lc.channelState.ChanType, | ||
) | ||
|
||
pd := &paymentDescriptor{ | ||
ChanID: htlc.ChanID, | ||
EntryType: Add, | ||
EntryType: entryType, | ||
RHash: PaymentHash(htlc.PaymentHash), | ||
Timeout: htlc.Expiry, | ||
Amount: htlc.Amount, | ||
LogIndex: lc.updateLogs.Remote.logIndex, | ||
HtlcIndex: lc.updateLogs.Remote.htlcCounter, | ||
OnionBlob: htlc.OnionBlob, | ||
BlindingPoint: htlc.BlindingPoint, | ||
CustomRecords: htlc.CustomRecords.Copy(), | ||
CustomRecords: customRecords, | ||
} | ||
|
||
localACKedIndex := lc.commitChains.Remote.tail().messageIndices.Local | ||
|
@@ -9825,7 +9960,7 @@ func (lc *LightningChannel) unsignedLocalUpdates(remoteMessageIndex, | |
|
||
// We don't save add updates as they are restored from the | ||
// remote commitment in restoreStateLogs. | ||
if pd.EntryType == Add { | ||
if pd.isAdd() { | ||
continue | ||
} | ||
|
||
|
@@ -9999,3 +10134,23 @@ func (lc *LightningChannel) ZeroConfRealScid() fn.Option[lnwire.ShortChannelID] | |
|
||
return fn.None[lnwire.ShortChannelID]() | ||
} | ||
|
||
// entryTypeForHtlc returns the add type that should be used for adding this | ||
// HTLC to the channel. If the channel has a tapscript root and the HTLC carries | ||
// the NoOp bit in the custom records then we'll convert this to a NoOp add. | ||
func (lc *LightningChannel) entryTypeForHtlc(records lnwire.CustomRecords, | ||
chanType channeldb.ChannelType) updateType { | ||
|
||
noopTLV := uint64(NoOpHtlcTLVEntry.TypeVal()) | ||
_, noopFlag := records[noopTLV] | ||
if noopFlag && chanType.HasTapscriptRoot() { | ||
return NoOpAdd | ||
} | ||
|
||
if noopFlag && !chanType.HasTapscriptRoot() { | ||
lc.log.Warnf("Received flag for noop-add over a channel that " + | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's make this an error since it's dangerous for the receiver? IIUC in tap chan we can also send normal HTLCs, I'm wondering if the receiver can refuse receiving noop Adds, say via a config. Or is there a way to check that if it's sending normal htlcs we disable it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One important thing to keep in mind here is that all of the code introduced in this PR is not used unless external software (i.e tapd in our case) explicitly tells LND to treat this as a noop HTLC (via the noop TLV). The control guards around whether the TLV is set lies externally. Specifically we only set it if the intent of the tap payment is clearly to send assets and not sats. If a normal (sats) payment is initiated we don't set the flag. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If we sign as if we're doing it, but they don't, then that leads to a force close just like if any of the other channel rendering attributes are ignored.
When this rolls out, there'll be a new feature bit used to signal if both sides understand the feature. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should return an error here instead. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can't really see why this is needed:
|
||
"doesn't have a tapscript root") | ||
} | ||
|
||
return Add | ||
} |
Uh oh!
There was an error while loading. Please reload this page.