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
|
# 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
|
## Functional Enhancements
|
||||||
|
|
||||||
* RPCs `walletrpc.EstimateFee` and `walletrpc.FundPsbt` now
|
* RPCs `walletrpc.EstimateFee` and `walletrpc.FundPsbt` now
|
||||||
|
|||||||
+26
-3
@@ -10,9 +10,20 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/tlv"
|
"github.com/lightningnetwork/lnd/tlv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// htlcCustomSigType is the TLV type that is used to encode the custom HTLC
|
var (
|
||||||
// signatures within the custom data for an existing HTLC.
|
// htlcCustomSigType is the TLV type that is used to encode the custom
|
||||||
var htlcCustomSigType tlv.TlvType65543
|
// 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
|
// AuxHtlcView is a struct that contains a safe copy of an HTLC view that can
|
||||||
// be used by aux components.
|
// be used by aux components.
|
||||||
@@ -116,6 +127,18 @@ func (a *AuxHtlcDescriptor) AddHeight(
|
|||||||
return a.addCommitHeightLocal
|
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
|
// 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
|
// commitment chain. The height is returned based on the chain the HTLC is being
|
||||||
// removed from (local or remote chain).
|
// removed from (local or remote chain).
|
||||||
|
|||||||
+171
-16
@@ -551,6 +551,12 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight,
|
|||||||
remoteOutputIndex = htlc.OutputIndex
|
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
|
// With the scripts reconstructed (depending on if this is our commit
|
||||||
// vs theirs or a pending commit for the remote party), we can now
|
// vs theirs or a pending commit for the remote party), we can now
|
||||||
// re-create the original payment descriptor.
|
// re-create the original payment descriptor.
|
||||||
@@ -559,7 +565,7 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight,
|
|||||||
RHash: htlc.RHash,
|
RHash: htlc.RHash,
|
||||||
Timeout: htlc.RefundTimeout,
|
Timeout: htlc.RefundTimeout,
|
||||||
Amount: htlc.Amt,
|
Amount: htlc.Amt,
|
||||||
EntryType: Add,
|
EntryType: entryType,
|
||||||
HtlcIndex: htlc.HtlcIndex,
|
HtlcIndex: htlc.HtlcIndex,
|
||||||
LogIndex: htlc.LogIndex,
|
LogIndex: htlc.LogIndex,
|
||||||
OnionBlob: htlc.OnionBlob,
|
OnionBlob: htlc.OnionBlob,
|
||||||
@@ -570,7 +576,7 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight,
|
|||||||
theirPkScript: theirP2WSH,
|
theirPkScript: theirP2WSH,
|
||||||
theirWitnessScript: theirWitnessScript,
|
theirWitnessScript: theirWitnessScript,
|
||||||
BlindingPoint: htlc.BlindingPoint,
|
BlindingPoint: htlc.BlindingPoint,
|
||||||
CustomRecords: htlc.CustomRecords.Copy(),
|
CustomRecords: customRecords,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1100,6 +1106,10 @@ func (lc *LightningChannel) logUpdateToPayDesc(logUpdate *channeldb.LogUpdate,
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pd.EntryType = lc.entryTypeForHtlc(
|
||||||
|
pd.CustomRecords, lc.channelState.ChanType,
|
||||||
|
)
|
||||||
|
|
||||||
isDustRemote := HtlcIsDust(
|
isDustRemote := HtlcIsDust(
|
||||||
lc.channelState.ChanType, false, lntypes.Remote,
|
lc.channelState.ChanType, false, lntypes.Remote,
|
||||||
feeRate, wireMsg.Amount.ToSatoshis(), remoteDustLimit,
|
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
|
// We don't need to generate an htlc script yet. This will be
|
||||||
// done once we sign our remote commitment.
|
// 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
|
// but this Add restoration was a no-op as every single one of
|
||||||
// these Adds was already restored since they're all incoming
|
// these Adds was already restored since they're all incoming
|
||||||
// htlcs on the local commitment.
|
// htlcs on the local commitment.
|
||||||
if payDesc.EntryType == Add {
|
if payDesc.isAdd() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1881,7 +1895,7 @@ func (lc *LightningChannel) restorePendingLocalUpdates(
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch payDesc.EntryType {
|
switch payDesc.EntryType {
|
||||||
case Add:
|
case Add, NoOpAdd:
|
||||||
// The HtlcIndex of the added HTLC _must_ be equal to
|
// The HtlcIndex of the added HTLC _must_ be equal to
|
||||||
// the log's htlcCounter at this point. If it is not we
|
// the log's htlcCounter at this point. If it is not we
|
||||||
// panic to catch this.
|
// panic to catch this.
|
||||||
@@ -2993,6 +3007,22 @@ func (lc *LightningChannel) evaluateHTLCView(view *HtlcView,
|
|||||||
)
|
)
|
||||||
if rmvHeight == 0 {
|
if rmvHeight == 0 {
|
||||||
switch {
|
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
|
// If an incoming HTLC is being settled, then
|
||||||
// this means that the preimage has been
|
// this means that the preimage has been
|
||||||
// received by the settling party Therefore, we
|
// received by the settling party Therefore, we
|
||||||
@@ -3030,7 +3060,7 @@ func (lc *LightningChannel) evaluateHTLCView(view *HtlcView,
|
|||||||
liveAdds := fn.Filter(
|
liveAdds := fn.Filter(
|
||||||
view.Updates.GetForParty(party),
|
view.Updates.GetForParty(party),
|
||||||
func(pd *paymentDescriptor) bool {
|
func(pd *paymentDescriptor) bool {
|
||||||
isAdd := pd.EntryType == Add
|
isAdd := pd.isAdd()
|
||||||
shouldSkip := skip.GetForParty(party).
|
shouldSkip := skip.GetForParty(party).
|
||||||
Contains(pd.HtlcIndex)
|
Contains(pd.HtlcIndex)
|
||||||
|
|
||||||
@@ -3069,7 +3099,7 @@ func (lc *LightningChannel) evaluateHTLCView(view *HtlcView,
|
|||||||
// corresponding to whoseCommitmentChain.
|
// corresponding to whoseCommitmentChain.
|
||||||
isUncommitted := func(update *paymentDescriptor) bool {
|
isUncommitted := func(update *paymentDescriptor) bool {
|
||||||
switch update.EntryType {
|
switch update.EntryType {
|
||||||
case Add:
|
case Add, NoOpAdd:
|
||||||
return update.addCommitHeights.GetForParty(
|
return update.addCommitHeights.GetForParty(
|
||||||
whoseCommitChain,
|
whoseCommitChain,
|
||||||
) == 0
|
) == 0
|
||||||
@@ -3145,6 +3175,92 @@ func (lc *LightningChannel) fetchParent(entry *paymentDescriptor,
|
|||||||
return addEntry, nil
|
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
|
// generateRemoteHtlcSigJobs generates a series of HTLC signature jobs for the
|
||||||
// sig pool, along with a channel that if closed, will cancel any jobs after
|
// 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
|
// 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
|
// Go through all updates, checking that they don't violate the
|
||||||
// channel constraints.
|
// channel constraints.
|
||||||
for _, entry := range updates {
|
for _, entry := range updates {
|
||||||
if entry.EntryType == Add {
|
if entry.isAdd() {
|
||||||
// An HTLC is being added, this will add to the
|
// An HTLC is being added, this will add to the
|
||||||
// number and amount in flight.
|
// number and amount in flight.
|
||||||
amtInFlight += entry.Amount
|
amtInFlight += entry.Amount
|
||||||
@@ -4668,6 +4784,15 @@ func (lc *LightningChannel) computeView(view *HtlcView,
|
|||||||
if whoseCommitChain == lntypes.Local &&
|
if whoseCommitChain == lntypes.Local &&
|
||||||
u.EntryType == Settle {
|
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)
|
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
|
// don't re-forward any already processed HTLC's after a
|
||||||
// restart.
|
// restart.
|
||||||
switch {
|
switch {
|
||||||
case pd.EntryType == Add && committedAdd && shouldFwdAdd:
|
case pd.isAdd() && committedAdd && shouldFwdAdd:
|
||||||
// Construct a reference specifying the location that
|
// Construct a reference specifying the location that
|
||||||
// this forwarded Add will be written in the forwarding
|
// this forwarded Add will be written in the forwarding
|
||||||
// package constructed at this remote height.
|
// package constructed at this remote height.
|
||||||
@@ -5731,7 +5856,7 @@ func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.RevokeAndAck) (
|
|||||||
addUpdatesToForward, pd.toLogUpdate(),
|
addUpdatesToForward, pd.toLogUpdate(),
|
||||||
)
|
)
|
||||||
|
|
||||||
case pd.EntryType != Add && committedRmv && shouldFwdRmv:
|
case !pd.isAdd() && committedRmv && shouldFwdRmv:
|
||||||
// Construct a reference specifying the location that
|
// Construct a reference specifying the location that
|
||||||
// this forwarded Settle/Fail will be written in the
|
// this forwarded Settle/Fail will be written in the
|
||||||
// forwarding package constructed at this remote height.
|
// 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.
|
// Grab all of our HTLCs and evaluate against the dust limit.
|
||||||
for e := lc.updateLogs.Local.Front(); e != nil; e = e.Next() {
|
for e := lc.updateLogs.Local.Front(); e != nil; e = e.Next() {
|
||||||
pd := e.Value
|
pd := e.Value
|
||||||
if pd.EntryType != Add {
|
if !pd.isAdd() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5989,7 +6114,7 @@ func (lc *LightningChannel) GetDustSum(whoseCommit lntypes.ChannelParty,
|
|||||||
// Grab all of their HTLCs and evaluate against the dust limit.
|
// Grab all of their HTLCs and evaluate against the dust limit.
|
||||||
for e := lc.updateLogs.Remote.Front(); e != nil; e = e.Next() {
|
for e := lc.updateLogs.Remote.Front(); e != nil; e = e.Next() {
|
||||||
pd := e.Value
|
pd := e.Value
|
||||||
if pd.EntryType != Add {
|
if !pd.isAdd() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6062,9 +6187,14 @@ func (lc *LightningChannel) MayAddOutgoingHtlc(amt lnwire.MilliSatoshi) error {
|
|||||||
func (lc *LightningChannel) htlcAddDescriptor(htlc *lnwire.UpdateAddHTLC,
|
func (lc *LightningChannel) htlcAddDescriptor(htlc *lnwire.UpdateAddHTLC,
|
||||||
openKey *models.CircuitKey) *paymentDescriptor {
|
openKey *models.CircuitKey) *paymentDescriptor {
|
||||||
|
|
||||||
|
customRecords := htlc.CustomRecords.Copy()
|
||||||
|
entryType := lc.entryTypeForHtlc(
|
||||||
|
customRecords, lc.channelState.ChanType,
|
||||||
|
)
|
||||||
|
|
||||||
return &paymentDescriptor{
|
return &paymentDescriptor{
|
||||||
ChanID: htlc.ChanID,
|
ChanID: htlc.ChanID,
|
||||||
EntryType: Add,
|
EntryType: entryType,
|
||||||
RHash: PaymentHash(htlc.PaymentHash),
|
RHash: PaymentHash(htlc.PaymentHash),
|
||||||
Timeout: htlc.Expiry,
|
Timeout: htlc.Expiry,
|
||||||
Amount: htlc.Amount,
|
Amount: htlc.Amount,
|
||||||
@@ -6073,7 +6203,7 @@ func (lc *LightningChannel) htlcAddDescriptor(htlc *lnwire.UpdateAddHTLC,
|
|||||||
OnionBlob: htlc.OnionBlob,
|
OnionBlob: htlc.OnionBlob,
|
||||||
OpenCircuitKey: openKey,
|
OpenCircuitKey: openKey,
|
||||||
BlindingPoint: htlc.BlindingPoint,
|
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)
|
lc.updateLogs.Remote.htlcCounter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customRecords := htlc.CustomRecords.Copy()
|
||||||
|
entryType := lc.entryTypeForHtlc(
|
||||||
|
customRecords, lc.channelState.ChanType,
|
||||||
|
)
|
||||||
|
|
||||||
pd := &paymentDescriptor{
|
pd := &paymentDescriptor{
|
||||||
ChanID: htlc.ChanID,
|
ChanID: htlc.ChanID,
|
||||||
EntryType: Add,
|
EntryType: entryType,
|
||||||
RHash: PaymentHash(htlc.PaymentHash),
|
RHash: PaymentHash(htlc.PaymentHash),
|
||||||
Timeout: htlc.Expiry,
|
Timeout: htlc.Expiry,
|
||||||
Amount: htlc.Amount,
|
Amount: htlc.Amount,
|
||||||
@@ -6136,7 +6271,7 @@ func (lc *LightningChannel) ReceiveHTLC(htlc *lnwire.UpdateAddHTLC) (uint64,
|
|||||||
HtlcIndex: lc.updateLogs.Remote.htlcCounter,
|
HtlcIndex: lc.updateLogs.Remote.htlcCounter,
|
||||||
OnionBlob: htlc.OnionBlob,
|
OnionBlob: htlc.OnionBlob,
|
||||||
BlindingPoint: htlc.BlindingPoint,
|
BlindingPoint: htlc.BlindingPoint,
|
||||||
CustomRecords: htlc.CustomRecords.Copy(),
|
CustomRecords: customRecords,
|
||||||
}
|
}
|
||||||
|
|
||||||
localACKedIndex := lc.commitChains.Remote.tail().messageIndices.Local
|
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
|
// We don't save add updates as they are restored from the
|
||||||
// remote commitment in restoreStateLogs.
|
// remote commitment in restoreStateLogs.
|
||||||
if pd.EntryType == Add {
|
if pd.isAdd() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9999,3 +10134,23 @@ func (lc *LightningChannel) ZeroConfRealScid() fn.Option[lnwire.ShortChannelID]
|
|||||||
|
|
||||||
return fn.None[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
|
// 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
|
// to re-send the CommitDiff. After the diff has been sent, both nodes should
|
||||||
// resynchronize and be able to complete the dangling commit.
|
// 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
|
// 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,
|
// unittest. The channel will be funded evenly with Alice having 5 BTC,
|
||||||
// and Bob 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
|
var fakeOnionBlob [lnwire.OnionPacketSize]byte
|
||||||
copy(fakeOnionBlob[:], bytes.Repeat([]byte{0x05}, lnwire.OnionPacketSize))
|
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
|
// We'll start off the scenario with Bob sending 3 HTLC's to Alice in a
|
||||||
// single state update.
|
// single state update.
|
||||||
htlcAmt := lnwire.NewMSatFromSatoshis(20000)
|
htlcAmt := lnwire.NewMSatFromSatoshis(20000)
|
||||||
@@ -3251,10 +3264,11 @@ func testChanSyncOweCommitment(t *testing.T, chanType channeldb.ChannelType) {
|
|||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
rHash := sha256.Sum256(bobPreimage[:])
|
rHash := sha256.Sum256(bobPreimage[:])
|
||||||
h := &lnwire.UpdateAddHTLC{
|
h := &lnwire.UpdateAddHTLC{
|
||||||
PaymentHash: rHash,
|
PaymentHash: rHash,
|
||||||
Amount: htlcAmt,
|
Amount: htlcAmt,
|
||||||
Expiry: uint32(10),
|
Expiry: uint32(10),
|
||||||
OnionBlob: fakeOnionBlob,
|
OnionBlob: fakeOnionBlob,
|
||||||
|
CustomRecords: records,
|
||||||
}
|
}
|
||||||
|
|
||||||
htlcIndex, err := bobChannel.AddHTLC(h, nil)
|
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)
|
t.Fatalf("unable to settle htlc: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var alicePreimage [32]byte
|
var alicePreimage [32]byte
|
||||||
copy(alicePreimage[:], bytes.Repeat([]byte{0xaa}, 32))
|
copy(alicePreimage[:], bytes.Repeat([]byte{0xaa}, 32))
|
||||||
rHash := sha256.Sum256(alicePreimage[:])
|
rHash := sha256.Sum256(alicePreimage[:])
|
||||||
aliceHtlc := &lnwire.UpdateAddHTLC{
|
aliceHtlc := &lnwire.UpdateAddHTLC{
|
||||||
ChanID: chanID,
|
ChanID: chanID,
|
||||||
PaymentHash: rHash,
|
PaymentHash: rHash,
|
||||||
Amount: htlcAmt,
|
Amount: htlcAmt,
|
||||||
Expiry: uint32(10),
|
Expiry: uint32(10),
|
||||||
OnionBlob: fakeOnionBlob,
|
OnionBlob: fakeOnionBlob,
|
||||||
|
CustomRecords: records,
|
||||||
}
|
}
|
||||||
aliceHtlcIndex, err := aliceChannel.AddHTLC(aliceHtlc, nil)
|
aliceHtlcIndex, err := aliceChannel.AddHTLC(aliceHtlc, nil)
|
||||||
require.NoError(t, err, "unable to add alice's htlc")
|
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
|
// At this point, the final balances of both parties should properly
|
||||||
// reflect the amount of HTLC's sent.
|
// reflect the amount of HTLC's sent.
|
||||||
bobMsatSent := numBobHtlcs * htlcAmt
|
if noop {
|
||||||
if aliceChannel.channelState.TotalMSatSent != htlcAmt {
|
// If this test-case includes noop HTLCs, then we don't expect
|
||||||
t.Fatalf("wrong value for msat sent: expected %v, got %v",
|
// any balance changes.
|
||||||
htlcAmt, aliceChannel.channelState.TotalMSatSent)
|
require.Zero(t, aliceChannel.channelState.TotalMSatSent)
|
||||||
}
|
require.Zero(t, aliceChannel.channelState.TotalMSatReceived)
|
||||||
if aliceChannel.channelState.TotalMSatReceived != bobMsatSent {
|
require.Zero(t, bobChannel.channelState.TotalMSatSent)
|
||||||
t.Fatalf("wrong value for msat recv: expected %v, got %v",
|
require.Zero(t, bobChannel.channelState.TotalMSatReceived)
|
||||||
bobMsatSent, aliceChannel.channelState.TotalMSatReceived)
|
} else {
|
||||||
}
|
// Otherwise, calculate the expected changes and assert them.
|
||||||
if bobChannel.channelState.TotalMSatSent != bobMsatSent {
|
bobMsatSent := numBobHtlcs * htlcAmt
|
||||||
t.Fatalf("wrong value for msat sent: expected %v, got %v",
|
|
||||||
bobMsatSent, bobChannel.channelState.TotalMSatSent)
|
aliceChan := aliceChannel.channelState
|
||||||
}
|
bobChan := bobChannel.channelState
|
||||||
if bobChannel.channelState.TotalMSatReceived != htlcAmt {
|
|
||||||
t.Fatalf("wrong value for msat recv: expected %v, got %v",
|
require.Equal(t, aliceChan.TotalMSatSent, htlcAmt)
|
||||||
htlcAmt, bobChannel.channelState.TotalMSatReceived)
|
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 {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
chanType channeldb.ChannelType
|
chanType channeldb.ChannelType
|
||||||
|
noop bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "tweakless",
|
name: "tweakless",
|
||||||
@@ -3571,10 +3591,18 @@ func TestChanSyncOweCommitment(t *testing.T) {
|
|||||||
channeldb.SimpleTaprootFeatureBit |
|
channeldb.SimpleTaprootFeatureBit |
|
||||||
channeldb.TapscriptRootBit,
|
channeldb.TapscriptRootBit,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "tapscript root with noop",
|
||||||
|
chanType: channeldb.SingleFunderTweaklessBit |
|
||||||
|
channeldb.AnchorOutputsBit |
|
||||||
|
channeldb.SimpleTaprootFeatureBit |
|
||||||
|
channeldb.TapscriptRootBit,
|
||||||
|
noop: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
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
|
// FeeUpdate is an update type sent by the channel initiator that
|
||||||
// updates the fee rate used when signing the commitment transaction.
|
// updates the fee rate used when signing the commitment transaction.
|
||||||
FeeUpdate
|
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
|
// String returns a human readable string that uniquely identifies the target
|
||||||
@@ -58,6 +65,8 @@ func (u updateType) String() string {
|
|||||||
return "Settle"
|
return "Settle"
|
||||||
case FeeUpdate:
|
case FeeUpdate:
|
||||||
return "FeeUpdate"
|
return "FeeUpdate"
|
||||||
|
case NoOpAdd:
|
||||||
|
return "NoOpAdd"
|
||||||
default:
|
default:
|
||||||
return "<unknown type>"
|
return "<unknown type>"
|
||||||
}
|
}
|
||||||
@@ -216,6 +225,14 @@ type paymentDescriptor struct {
|
|||||||
// into the log to the HTLC being modified.
|
// into the log to the HTLC being modified.
|
||||||
EntryType updateType
|
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
|
// isForwarded denotes if an incoming HTLC has been forwarded to any
|
||||||
// possible upstream peers in the route.
|
// possible upstream peers in the route.
|
||||||
isForwarded bool
|
isForwarded bool
|
||||||
@@ -238,7 +255,7 @@ type paymentDescriptor struct {
|
|||||||
func (pd *paymentDescriptor) toLogUpdate() channeldb.LogUpdate {
|
func (pd *paymentDescriptor) toLogUpdate() channeldb.LogUpdate {
|
||||||
var msg lnwire.Message
|
var msg lnwire.Message
|
||||||
switch pd.EntryType {
|
switch pd.EntryType {
|
||||||
case Add:
|
case Add, NoOpAdd:
|
||||||
msg = &lnwire.UpdateAddHTLC{
|
msg = &lnwire.UpdateAddHTLC{
|
||||||
ChanID: pd.ChanID,
|
ChanID: pd.ChanID,
|
||||||
ID: pd.HtlcIndex,
|
ID: pd.HtlcIndex,
|
||||||
@@ -290,7 +307,7 @@ func (pd *paymentDescriptor) setCommitHeight(
|
|||||||
whoseCommitChain lntypes.ChannelParty, nextHeight uint64) {
|
whoseCommitChain lntypes.ChannelParty, nextHeight uint64) {
|
||||||
|
|
||||||
switch pd.EntryType {
|
switch pd.EntryType {
|
||||||
case Add:
|
case Add, NoOpAdd:
|
||||||
pd.addCommitHeights.SetForParty(
|
pd.addCommitHeights.SetForParty(
|
||||||
whoseCommitChain, nextHeight,
|
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