Merge pull request #9871 from GeorgeTsagk/htlc-noop-add
Add `NoopAdd` HTLCs
This commit is contained in:
5 files changed
+673
-48
No files matched your search
@@ -40,6 +40,13 @@
|
||||
|
||||
# New Features
|
||||
|
||||
- Added [NoOp HTLCs](https://github.com/lightningnetwork/lnd/pull/9871). This
|
||||
allows sending HTLCs to the remote party without shifting the balances of the
|
||||
channel. This is currently only possible to use with custom channels, and only
|
||||
when the appropriate TLV flag is set. This allows for HTLCs carrying metadata to
|
||||
reflect their state on the channel commitment without having to send or receive
|
||||
a certain amount of msats.
|
||||
|
||||
## Functional Enhancements
|
||||
|
||||
* RPCs `walletrpc.EstimateFee` and `walletrpc.FundPsbt` now
|
||||
|
||||
+26
-3
@@ -10,9 +10,20 @@ import (
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
)
|
||||
|
||||
// htlcCustomSigType is the TLV type that is used to encode the custom HTLC
|
||||
// signatures within the custom data for an existing HTLC.
|
||||
var htlcCustomSigType tlv.TlvType65543
|
||||
var (
|
||||
// htlcCustomSigType is the TLV type that is used to encode the custom
|
||||
// HTLC signatures within the custom data for an existing HTLC.
|
||||
htlcCustomSigType tlv.TlvType65543
|
||||
|
||||
// NoOpHtlcTLVEntry is the TLV that that's used in the update_add_htlc
|
||||
// message to indicate the presence of a noop HTLC. This has no encoded
|
||||
// value, its presence is used to indicate that the HTLC is a noop.
|
||||
NoOpHtlcTLVEntry tlv.TlvType65544
|
||||
)
|
||||
|
||||
// NoOpHtlcTLVType is the (golang) type of the TLV record that's used to signal
|
||||
// that an HTLC should be a noop HTLC.
|
||||
type NoOpHtlcTLVType = tlv.TlvType65544
|
||||
|
||||
// AuxHtlcView is a struct that contains a safe copy of an HTLC view that can
|
||||
// be used by aux components.
|
||||
@@ -116,6 +127,18 @@ func (a *AuxHtlcDescriptor) AddHeight(
|
||||
return a.addCommitHeightLocal
|
||||
}
|
||||
|
||||
// IsAdd checks if the entry type of the Aux HTLC Descriptor is an add type.
|
||||
func (a *AuxHtlcDescriptor) IsAdd() bool {
|
||||
switch a.EntryType {
|
||||
case Add:
|
||||
fallthrough
|
||||
case NoOpAdd:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveHeight returns the height at which the HTLC was removed from the
|
||||
// commitment chain. The height is returned based on the chain the HTLC is being
|
||||
// removed from (local or remote chain).
|
||||
|
||||
+171
-16
@@ -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 &&
|
||||
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,
|
||||
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()
|
||||
|
||||
// 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
|
||||
if delta >= 0 {
|
||||
totalLocal += lnwire.MilliSatoshi(delta)
|
||||
} else {
|
||||
totalLocal -= lnwire.MilliSatoshi(-1 * delta)
|
||||
}
|
||||
|
||||
return totalLocal > localReserve
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
// 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() {
|
||||
// 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 {
|
||||
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,
|
||||
)
|
||||
|
||||
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,9 +6256,14 @@ 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,
|
||||
@@ -6136,7 +6271,7 @@ func (lc *LightningChannel) ReceiveHTLC(htlc *lnwire.UpdateAddHTLC) (uint64,
|
||||
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 " +
|
||||
"doesn't have a tapscript root")
|
||||
}
|
||||
|
||||
return Add
|
||||
}
|
||||
+445
-27
@@ -3232,7 +3232,9 @@ func restartChannel(channelOld *LightningChannel) (*LightningChannel, error) {
|
||||
// he receives Alice's CommitSig message, then Alice concludes that she needs
|
||||
// to re-send the CommitDiff. After the diff has been sent, both nodes should
|
||||
// resynchronize and be able to complete the dangling commit.
|
||||
func testChanSyncOweCommitment(t *testing.T, chanType channeldb.ChannelType) {
|
||||
func testChanSyncOweCommitment(t *testing.T,
|
||||
chanType channeldb.ChannelType, noop bool) {
|
||||
|
||||
// Create a test channel which will be used for the duration of this
|
||||
// unittest. The channel will be funded evenly with Alice having 5 BTC,
|
||||
// and Bob having 5 BTC.
|
||||
@@ -3242,6 +3244,17 @@ func testChanSyncOweCommitment(t *testing.T, chanType channeldb.ChannelType) {
|
||||
var fakeOnionBlob [lnwire.OnionPacketSize]byte
|
||||
copy(fakeOnionBlob[:], bytes.Repeat([]byte{0x05}, lnwire.OnionPacketSize))
|
||||
|
||||
// Let's create the noop add TLV record. This will only be
|
||||
// effective for channels that have a tapscript root.
|
||||
noopRecord := tlv.NewPrimitiveRecord[NoOpHtlcTLVType, bool](true)
|
||||
records, err := tlv.RecordsToMap([]tlv.Record{noopRecord.Record()})
|
||||
require.NoError(t, err)
|
||||
|
||||
// If the noop flag is not set for this test, nullify the records.
|
||||
if !noop {
|
||||
records = nil
|
||||
}
|
||||
|
||||
// We'll start off the scenario with Bob sending 3 HTLC's to Alice in a
|
||||
// single state update.
|
||||
htlcAmt := lnwire.NewMSatFromSatoshis(20000)
|
||||
@@ -3251,10 +3264,11 @@ func testChanSyncOweCommitment(t *testing.T, chanType channeldb.ChannelType) {
|
||||
for i := 0; i < 3; i++ {
|
||||
rHash := sha256.Sum256(bobPreimage[:])
|
||||
h := &lnwire.UpdateAddHTLC{
|
||||
PaymentHash: rHash,
|
||||
Amount: htlcAmt,
|
||||
Expiry: uint32(10),
|
||||
OnionBlob: fakeOnionBlob,
|
||||
PaymentHash: rHash,
|
||||
Amount: htlcAmt,
|
||||
Expiry: uint32(10),
|
||||
OnionBlob: fakeOnionBlob,
|
||||
CustomRecords: records,
|
||||
}
|
||||
|
||||
htlcIndex, err := bobChannel.AddHTLC(h, nil)
|
||||
@@ -3290,15 +3304,17 @@ func testChanSyncOweCommitment(t *testing.T, chanType channeldb.ChannelType) {
|
||||
t.Fatalf("unable to settle htlc: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var alicePreimage [32]byte
|
||||
copy(alicePreimage[:], bytes.Repeat([]byte{0xaa}, 32))
|
||||
rHash := sha256.Sum256(alicePreimage[:])
|
||||
aliceHtlc := &lnwire.UpdateAddHTLC{
|
||||
ChanID: chanID,
|
||||
PaymentHash: rHash,
|
||||
Amount: htlcAmt,
|
||||
Expiry: uint32(10),
|
||||
OnionBlob: fakeOnionBlob,
|
||||
ChanID: chanID,
|
||||
PaymentHash: rHash,
|
||||
Amount: htlcAmt,
|
||||
Expiry: uint32(10),
|
||||
OnionBlob: fakeOnionBlob,
|
||||
CustomRecords: records,
|
||||
}
|
||||
aliceHtlcIndex, err := aliceChannel.AddHTLC(aliceHtlc, nil)
|
||||
require.NoError(t, err, "unable to add alice's htlc")
|
||||
@@ -3519,22 +3535,25 @@ func testChanSyncOweCommitment(t *testing.T, chanType channeldb.ChannelType) {
|
||||
|
||||
// At this point, the final balances of both parties should properly
|
||||
// reflect the amount of HTLC's sent.
|
||||
bobMsatSent := numBobHtlcs * htlcAmt
|
||||
if aliceChannel.channelState.TotalMSatSent != htlcAmt {
|
||||
t.Fatalf("wrong value for msat sent: expected %v, got %v",
|
||||
htlcAmt, aliceChannel.channelState.TotalMSatSent)
|
||||
}
|
||||
if aliceChannel.channelState.TotalMSatReceived != bobMsatSent {
|
||||
t.Fatalf("wrong value for msat recv: expected %v, got %v",
|
||||
bobMsatSent, aliceChannel.channelState.TotalMSatReceived)
|
||||
}
|
||||
if bobChannel.channelState.TotalMSatSent != bobMsatSent {
|
||||
t.Fatalf("wrong value for msat sent: expected %v, got %v",
|
||||
bobMsatSent, bobChannel.channelState.TotalMSatSent)
|
||||
}
|
||||
if bobChannel.channelState.TotalMSatReceived != htlcAmt {
|
||||
t.Fatalf("wrong value for msat recv: expected %v, got %v",
|
||||
htlcAmt, bobChannel.channelState.TotalMSatReceived)
|
||||
if noop {
|
||||
// If this test-case includes noop HTLCs, then we don't expect
|
||||
// any balance changes.
|
||||
require.Zero(t, aliceChannel.channelState.TotalMSatSent)
|
||||
require.Zero(t, aliceChannel.channelState.TotalMSatReceived)
|
||||
require.Zero(t, bobChannel.channelState.TotalMSatSent)
|
||||
require.Zero(t, bobChannel.channelState.TotalMSatReceived)
|
||||
} else {
|
||||
// Otherwise, calculate the expected changes and assert them.
|
||||
bobMsatSent := numBobHtlcs * htlcAmt
|
||||
|
||||
aliceChan := aliceChannel.channelState
|
||||
bobChan := bobChannel.channelState
|
||||
|
||||
require.Equal(t, aliceChan.TotalMSatSent, htlcAmt)
|
||||
require.Equal(t, aliceChan.TotalMSatReceived, bobMsatSent)
|
||||
|
||||
require.Equal(t, bobChan.TotalMSatSent, bobMsatSent)
|
||||
require.Equal(t, bobChan.TotalMSatReceived, htlcAmt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3548,6 +3567,7 @@ func TestChanSyncOweCommitment(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
chanType channeldb.ChannelType
|
||||
noop bool
|
||||
}{
|
||||
{
|
||||
name: "tweakless",
|
||||
@@ -3571,10 +3591,18 @@ func TestChanSyncOweCommitment(t *testing.T) {
|
||||
channeldb.SimpleTaprootFeatureBit |
|
||||
channeldb.TapscriptRootBit,
|
||||
},
|
||||
{
|
||||
name: "tapscript root with noop",
|
||||
chanType: channeldb.SingleFunderTweaklessBit |
|
||||
channeldb.AnchorOutputsBit |
|
||||
channeldb.SimpleTaprootFeatureBit |
|
||||
channeldb.TapscriptRootBit,
|
||||
noop: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
testChanSyncOweCommitment(t, tc.chanType)
|
||||
testChanSyncOweCommitment(t, tc.chanType, tc.noop)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -11339,3 +11367,393 @@ func TestCreateCooperativeCloseTx(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNoopAddSettle tests that adding and settling an HTLC with no-op, no
|
||||
// balances are actually affected.
|
||||
func TestNoopAddSettle(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a test channel which will be used for the duration of this
|
||||
// unittest. The channel will be funded evenly with Alice having 5 BTC,
|
||||
// and Bob having 5 BTC.
|
||||
chanType := channeldb.SimpleTaprootFeatureBit |
|
||||
channeldb.AnchorOutputsBit | channeldb.ZeroHtlcTxFeeBit |
|
||||
channeldb.SingleFunderTweaklessBit | channeldb.TapscriptRootBit
|
||||
aliceChannel, bobChannel, err := CreateTestChannels(
|
||||
t, chanType,
|
||||
)
|
||||
require.NoError(t, err, "unable to create test channels")
|
||||
|
||||
const htlcAmt = 10_000
|
||||
htlc, preimage := createHTLC(0, htlcAmt)
|
||||
noopRecord := tlv.NewPrimitiveRecord[tlv.TlvType65544, bool](true)
|
||||
|
||||
records, err := tlv.RecordsToMap([]tlv.Record{noopRecord.Record()})
|
||||
require.NoError(t, err)
|
||||
htlc.CustomRecords = records
|
||||
|
||||
aliceBalance := aliceChannel.channelState.LocalCommitment.LocalBalance
|
||||
bobBalance := bobChannel.channelState.LocalCommitment.LocalBalance
|
||||
|
||||
// Have Alice add the HTLC, then lock it in with a new state transition.
|
||||
aliceHtlcIndex, err := aliceChannel.AddHTLC(htlc, nil)
|
||||
require.NoError(t, err, "alice unable to add htlc")
|
||||
bobHtlcIndex, err := bobChannel.ReceiveHTLC(htlc)
|
||||
require.NoError(t, err, "bob unable to receive htlc")
|
||||
|
||||
err = ForceStateTransition(aliceChannel, bobChannel)
|
||||
require.NoError(t, err)
|
||||
|
||||
// We'll have Bob settle the HTLC, then force another state transition.
|
||||
err = bobChannel.SettleHTLC(preimage, bobHtlcIndex, nil, nil, nil)
|
||||
require.NoError(t, err, "bob unable to settle inbound htlc")
|
||||
err = aliceChannel.ReceiveHTLCSettle(preimage, aliceHtlcIndex)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ForceStateTransition(aliceChannel, bobChannel)
|
||||
require.NoError(t, err)
|
||||
|
||||
aliceBalanceFinal := aliceChannel.channelState.LocalCommitment.LocalBalance //nolint:ll
|
||||
bobBalanceFinal := bobChannel.channelState.LocalCommitment.LocalBalance
|
||||
|
||||
// The balances of Alice and Bob should be the exact same and shouldn't
|
||||
// have changed.
|
||||
require.Equal(t, aliceBalance, aliceBalanceFinal)
|
||||
require.Equal(t, bobBalance, bobBalanceFinal)
|
||||
}
|
||||
|
||||
// TestNoopAddBelowReserve tests that the noop HTLCs behave as expected when
|
||||
// added over a channel where a party is below their reserve.
|
||||
func TestNoopAddBelowReserve(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a test channel which will be used for the duration of this
|
||||
// unittest. The channel will be funded evenly with Alice having 5 BTC,
|
||||
// and Bob having 5 BTC.
|
||||
chanType := channeldb.SimpleTaprootFeatureBit |
|
||||
channeldb.AnchorOutputsBit | channeldb.ZeroHtlcTxFeeBit |
|
||||
channeldb.SingleFunderTweaklessBit | channeldb.TapscriptRootBit
|
||||
aliceChan, bobChan, err := CreateTestChannels(t, chanType)
|
||||
require.NoError(t, err, "unable to create test channels")
|
||||
|
||||
aliceBalance := aliceChan.channelState.LocalCommitment.LocalBalance
|
||||
bobBalance := bobChan.channelState.LocalCommitment.LocalBalance
|
||||
|
||||
const (
|
||||
// htlcAmt is the default HTLC amount to be used, epxressed in
|
||||
// milli-satoshis.
|
||||
htlcAmt = lnwire.MilliSatoshi(500_000)
|
||||
|
||||
// numHtlc is the total number of HTLCs to be added/settled over
|
||||
// the channel.
|
||||
numHtlc = 20
|
||||
)
|
||||
|
||||
// Let's create the noop add TLV record to be used in all added HTLCs
|
||||
// over the channel.
|
||||
noopRecord := tlv.NewPrimitiveRecord[NoOpHtlcTLVType, bool](true)
|
||||
records, err := tlv.RecordsToMap([]tlv.Record{noopRecord.Record()})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Let's set Bob's reserve to whatever his local balance is, plus half
|
||||
// of the total amount to be added by the total HTLCs. This way we can
|
||||
// also verify that the noop-adds will start the nullification only once
|
||||
// Bob is above reserve.
|
||||
reserveTarget := (numHtlc / 2) * htlcAmt
|
||||
bobReserve := bobBalance + reserveTarget
|
||||
|
||||
bobChan.channelState.LocalChanCfg.ChanReserve =
|
||||
bobReserve.ToSatoshis()
|
||||
|
||||
aliceChan.channelState.RemoteChanCfg.ChanReserve =
|
||||
bobReserve.ToSatoshis()
|
||||
|
||||
// Add and settle all the HTLCs over the channel.
|
||||
for i := range numHtlc {
|
||||
htlc, preimage := createHTLC(i, htlcAmt)
|
||||
htlc.CustomRecords = records
|
||||
|
||||
aliceHtlcIndex, err := aliceChan.AddHTLC(htlc, nil)
|
||||
require.NoError(t, err, "alice unable to add htlc")
|
||||
bobHtlcIndex, err := bobChan.ReceiveHTLC(htlc)
|
||||
require.NoError(t, err, "bob unable to receive htlc")
|
||||
|
||||
require.NoError(t, ForceStateTransition(aliceChan, bobChan))
|
||||
|
||||
// We'll have Bob settle the HTLC, then force another state
|
||||
// transition.
|
||||
err = bobChan.SettleHTLC(preimage, bobHtlcIndex, nil, nil, nil)
|
||||
require.NoError(t, err, "bob unable to settle inbound htlc")
|
||||
err = aliceChan.ReceiveHTLCSettle(preimage, aliceHtlcIndex)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, ForceStateTransition(aliceChan, bobChan))
|
||||
}
|
||||
|
||||
// We need to kick the state transition one last time for the balances
|
||||
// to be updated on both commitments.
|
||||
require.NoError(t, ForceStateTransition(aliceChan, bobChan))
|
||||
|
||||
aliceBalanceFinal := aliceChan.channelState.LocalCommitment.LocalBalance
|
||||
bobBalanceFinal := bobChan.channelState.LocalCommitment.LocalBalance
|
||||
|
||||
// The balances of Alice and Bob must have changed exactly by half the
|
||||
// total number of HTLCs we added over the channel, plus one to get Bob
|
||||
// above the reserve. Bob's final balance should be as much as his
|
||||
// reserve plus one extra default HTLC amount.
|
||||
require.Equal(t, aliceBalance-htlcAmt*(numHtlc/2+1), aliceBalanceFinal)
|
||||
require.Equal(t, bobBalance+htlcAmt*(numHtlc/2+1), bobBalanceFinal)
|
||||
require.Equal(
|
||||
t, bobBalanceFinal.ToSatoshis(),
|
||||
bobChan.LocalChanReserve()+htlcAmt.ToSatoshis(),
|
||||
)
|
||||
}
|
||||
|
||||
// TestEvaluateNoOpHtlc tests that the noop htlc evaluator helper function
|
||||
// produces the expected balance deltas from various starting states.
|
||||
func TestEvaluateNoOpHtlc(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
localBalance, remoteBalance btcutil.Amount
|
||||
localReserve, remoteReserve btcutil.Amount
|
||||
entry *paymentDescriptor
|
||||
receiver lntypes.ChannelParty
|
||||
balanceDeltas *lntypes.Dual[int64]
|
||||
expectedDeltas *lntypes.Dual[int64]
|
||||
}{
|
||||
{
|
||||
name: "local above reserve",
|
||||
entry: &paymentDescriptor{
|
||||
Amount: lnwire.MilliSatoshi(2500),
|
||||
},
|
||||
receiver: lntypes.Local,
|
||||
balanceDeltas: &lntypes.Dual[int64]{
|
||||
Local: 0,
|
||||
Remote: 0,
|
||||
},
|
||||
expectedDeltas: &lntypes.Dual[int64]{
|
||||
Local: 0,
|
||||
Remote: 2_500,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remote above reserve",
|
||||
entry: &paymentDescriptor{
|
||||
Amount: lnwire.MilliSatoshi(2500),
|
||||
},
|
||||
receiver: lntypes.Remote,
|
||||
balanceDeltas: &lntypes.Dual[int64]{
|
||||
Local: 0,
|
||||
Remote: 0,
|
||||
},
|
||||
expectedDeltas: &lntypes.Dual[int64]{
|
||||
Local: 2_500,
|
||||
Remote: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "local below reserve",
|
||||
entry: &paymentDescriptor{
|
||||
Amount: lnwire.MilliSatoshi(2500),
|
||||
},
|
||||
receiver: lntypes.Local,
|
||||
localBalance: 25_000,
|
||||
localReserve: 50_000,
|
||||
balanceDeltas: &lntypes.Dual[int64]{
|
||||
Local: 0,
|
||||
Remote: 0,
|
||||
},
|
||||
expectedDeltas: &lntypes.Dual[int64]{
|
||||
Local: 2_500,
|
||||
Remote: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remote below reserve",
|
||||
entry: &paymentDescriptor{
|
||||
Amount: lnwire.MilliSatoshi(2500),
|
||||
},
|
||||
receiver: lntypes.Remote,
|
||||
remoteBalance: 25_000,
|
||||
remoteReserve: 50_000,
|
||||
balanceDeltas: &lntypes.Dual[int64]{
|
||||
Local: 0,
|
||||
Remote: 0,
|
||||
},
|
||||
expectedDeltas: &lntypes.Dual[int64]{
|
||||
Local: 0,
|
||||
Remote: 2_500,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "local above reserve with delta",
|
||||
entry: &paymentDescriptor{
|
||||
Amount: lnwire.MilliSatoshi(2500),
|
||||
},
|
||||
receiver: lntypes.Local,
|
||||
localBalance: 25_000,
|
||||
localReserve: 50_000,
|
||||
balanceDeltas: &lntypes.Dual[int64]{
|
||||
Local: 25_001_000,
|
||||
Remote: 0,
|
||||
},
|
||||
expectedDeltas: &lntypes.Dual[int64]{
|
||||
Local: 25_001_000,
|
||||
Remote: 2_500,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remote above reserve with delta",
|
||||
entry: &paymentDescriptor{
|
||||
Amount: lnwire.MilliSatoshi(2500),
|
||||
},
|
||||
receiver: lntypes.Remote,
|
||||
remoteBalance: 25_000,
|
||||
remoteReserve: 50_000,
|
||||
balanceDeltas: &lntypes.Dual[int64]{
|
||||
Local: 0,
|
||||
Remote: 25_001_000,
|
||||
},
|
||||
expectedDeltas: &lntypes.Dual[int64]{
|
||||
Local: 2_500,
|
||||
Remote: 25_001_000,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "local below reserve with delta",
|
||||
entry: &paymentDescriptor{
|
||||
Amount: lnwire.MilliSatoshi(2500),
|
||||
},
|
||||
receiver: lntypes.Local,
|
||||
localBalance: 25_000,
|
||||
localReserve: 50_000,
|
||||
balanceDeltas: &lntypes.Dual[int64]{
|
||||
Local: 24_999_000,
|
||||
Remote: 0,
|
||||
},
|
||||
expectedDeltas: &lntypes.Dual[int64]{
|
||||
Local: 25_001_500,
|
||||
Remote: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remote below reserve with delta",
|
||||
entry: &paymentDescriptor{
|
||||
Amount: lnwire.MilliSatoshi(2500),
|
||||
},
|
||||
receiver: lntypes.Remote,
|
||||
remoteBalance: 25_000,
|
||||
remoteReserve: 50_000,
|
||||
balanceDeltas: &lntypes.Dual[int64]{
|
||||
Local: 0,
|
||||
Remote: 24_998_000,
|
||||
},
|
||||
expectedDeltas: &lntypes.Dual[int64]{
|
||||
Local: 0,
|
||||
Remote: 25_000_500,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "local above reserve with negative delta",
|
||||
entry: &paymentDescriptor{
|
||||
Amount: lnwire.MilliSatoshi(2500),
|
||||
},
|
||||
receiver: lntypes.Remote,
|
||||
localBalance: 55_000,
|
||||
localReserve: 50_000,
|
||||
balanceDeltas: &lntypes.Dual[int64]{
|
||||
Local: -4_999_000,
|
||||
Remote: 0,
|
||||
},
|
||||
expectedDeltas: &lntypes.Dual[int64]{
|
||||
Local: -4_999_000,
|
||||
Remote: 2_500,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remote above reserve with negative delta",
|
||||
entry: &paymentDescriptor{
|
||||
Amount: lnwire.MilliSatoshi(2500),
|
||||
},
|
||||
receiver: lntypes.Remote,
|
||||
remoteBalance: 55_000,
|
||||
remoteReserve: 50_000,
|
||||
balanceDeltas: &lntypes.Dual[int64]{
|
||||
Local: 0,
|
||||
Remote: -4_999_000,
|
||||
},
|
||||
expectedDeltas: &lntypes.Dual[int64]{
|
||||
Local: 2_500,
|
||||
Remote: -4_999_000,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "local below reserve with negative delta",
|
||||
entry: &paymentDescriptor{
|
||||
Amount: lnwire.MilliSatoshi(2500),
|
||||
},
|
||||
receiver: lntypes.Local,
|
||||
localBalance: 55_000,
|
||||
localReserve: 50_000,
|
||||
balanceDeltas: &lntypes.Dual[int64]{
|
||||
Local: -5_001_000,
|
||||
Remote: 0,
|
||||
},
|
||||
expectedDeltas: &lntypes.Dual[int64]{
|
||||
Local: -4_998_500,
|
||||
Remote: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remote below reserve with negative delta",
|
||||
entry: &paymentDescriptor{
|
||||
Amount: lnwire.MilliSatoshi(2500),
|
||||
},
|
||||
receiver: lntypes.Remote,
|
||||
remoteBalance: 55_000,
|
||||
remoteReserve: 50_000,
|
||||
balanceDeltas: &lntypes.Dual[int64]{
|
||||
Local: 0,
|
||||
Remote: -5_001_000,
|
||||
},
|
||||
expectedDeltas: &lntypes.Dual[int64]{
|
||||
Local: 0,
|
||||
Remote: -4_998_500,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
chanType := channeldb.SimpleTaprootFeatureBit |
|
||||
channeldb.AnchorOutputsBit | channeldb.ZeroHtlcTxFeeBit |
|
||||
channeldb.SingleFunderTweaklessBit | channeldb.TapscriptRootBit
|
||||
aliceChan, _, err := CreateTestChannels(t, chanType)
|
||||
require.NoError(t, err, "unable to create test channels")
|
||||
|
||||
for _, testCase := range testCases {
|
||||
tc := testCase
|
||||
|
||||
t.Logf("Running test case: %s", testCase.name)
|
||||
|
||||
if tc.localBalance != 0 && tc.localReserve != 0 {
|
||||
aliceChan.channelState.LocalChanCfg.ChanReserve =
|
||||
tc.localReserve
|
||||
|
||||
aliceChan.channelState.LocalCommitment.LocalBalance =
|
||||
lnwire.NewMSatFromSatoshis(tc.localBalance)
|
||||
}
|
||||
|
||||
if tc.remoteBalance != 0 && tc.remoteReserve != 0 {
|
||||
aliceChan.channelState.RemoteChanCfg.ChanReserve =
|
||||
tc.remoteReserve
|
||||
|
||||
aliceChan.channelState.RemoteCommitment.RemoteBalance =
|
||||
lnwire.NewMSatFromSatoshis(tc.remoteBalance)
|
||||
}
|
||||
|
||||
aliceChan.evaluateNoOpHtlc(
|
||||
tc.entry, tc.receiver, tc.balanceDeltas,
|
||||
)
|
||||
|
||||
require.Equal(t, tc.expectedDeltas, tc.balanceDeltas)
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,13 @@ const (
|
||||
// FeeUpdate is an update type sent by the channel initiator that
|
||||
// updates the fee rate used when signing the commitment transaction.
|
||||
FeeUpdate
|
||||
|
||||
// NoOpAdd is an update type that adds a new HTLC entry into the log.
|
||||
// This differs from the normal Add type, in that when settled the
|
||||
// balance may go back to the sender, rather than be credited for the
|
||||
// receiver. The criteria about whether the balance will go back to the
|
||||
// sender is whether the receiver is sitting above the channel reserve.
|
||||
NoOpAdd
|
||||
)
|
||||
|
||||
// String returns a human readable string that uniquely identifies the target
|
||||
@@ -58,6 +65,8 @@ func (u updateType) String() string {
|
||||
return "Settle"
|
||||
case FeeUpdate:
|
||||
return "FeeUpdate"
|
||||
case NoOpAdd:
|
||||
return "NoOpAdd"
|
||||
default:
|
||||
return "<unknown type>"
|
||||
}
|
||||
@@ -216,6 +225,14 @@ type paymentDescriptor struct {
|
||||
// into the log to the HTLC being modified.
|
||||
EntryType updateType
|
||||
|
||||
// noOpSettle is a flag indicating whether a chain of entries resulted
|
||||
// in an effective no-op settle. That means that the amount was credited
|
||||
// back to the sender. This is useful as we need a way to mark whether
|
||||
// the noop add was effective, which can be useful at later stages,
|
||||
// where we might not be able to re-run the criteria for the
|
||||
// effectiveness of the noop-add.
|
||||
noOpSettle bool
|
||||
|
||||
// isForwarded denotes if an incoming HTLC has been forwarded to any
|
||||
// possible upstream peers in the route.
|
||||
isForwarded bool
|
||||
@@ -238,7 +255,7 @@ type paymentDescriptor struct {
|
||||
func (pd *paymentDescriptor) toLogUpdate() channeldb.LogUpdate {
|
||||
var msg lnwire.Message
|
||||
switch pd.EntryType {
|
||||
case Add:
|
||||
case Add, NoOpAdd:
|
||||
msg = &lnwire.UpdateAddHTLC{
|
||||
ChanID: pd.ChanID,
|
||||
ID: pd.HtlcIndex,
|
||||
@@ -290,7 +307,7 @@ func (pd *paymentDescriptor) setCommitHeight(
|
||||
whoseCommitChain lntypes.ChannelParty, nextHeight uint64) {
|
||||
|
||||
switch pd.EntryType {
|
||||
case Add:
|
||||
case Add, NoOpAdd:
|
||||
pd.addCommitHeights.SetForParty(
|
||||
whoseCommitChain, nextHeight,
|
||||
)
|
||||
@@ -311,3 +328,8 @@ func (pd *paymentDescriptor) setCommitHeight(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// isAdd returns true if the paymentDescriptor is of type Add.
|
||||
func (pd *paymentDescriptor) isAdd() bool {
|
||||
return pd.EntryType == Add || pd.EntryType == NoOpAdd
|
||||
}
|
||||
Reference in new issue
Block a user