Merge pull request #5770 from Crypt-iQ/dust_threshold_0619
lnwallet+htlcswitch: make Switch dust-aware
This commit is contained in:
16 files changed
+1120
-39
No files matched your search
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/lnpeer"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/record"
|
||||
)
|
||||
@@ -57,6 +58,21 @@ type packetHandler interface {
|
||||
handleLocalAddPacket(*htlcPacket) error
|
||||
}
|
||||
|
||||
// dustHandler is an interface used exclusively by the Switch to evaluate
|
||||
// whether a link has too much dust exposure.
|
||||
type dustHandler interface {
|
||||
// getDustSum returns the dust sum on either the local or remote
|
||||
// commitment.
|
||||
getDustSum(remote bool) lnwire.MilliSatoshi
|
||||
|
||||
// getFeeRate returns the current channel feerate.
|
||||
getFeeRate() chainfee.SatPerKWeight
|
||||
|
||||
// getDustClosure returns a closure that can evaluate whether a passed
|
||||
// HTLC is dust.
|
||||
getDustClosure() dustClosure
|
||||
}
|
||||
|
||||
// ChannelUpdateHandler is an interface that provides methods that allow
|
||||
// sending lnwire.Message to the underlying link as well as querying state.
|
||||
type ChannelUpdateHandler interface {
|
||||
@@ -122,6 +138,9 @@ type ChannelLink interface {
|
||||
// Embed the ChannelUpdateHandler interface.
|
||||
ChannelUpdateHandler
|
||||
|
||||
// Embed the dustHandler interface.
|
||||
dustHandler
|
||||
|
||||
// ChannelPoint returns the channel outpoint for the channel link.
|
||||
ChannelPoint() *wire.OutPoint
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btclog"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/lightningnetwork/lnd/build"
|
||||
@@ -1929,6 +1930,10 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) {
|
||||
"error receiving fee update: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Update the mailbox's feerate as well.
|
||||
l.mailBox.SetFeeRate(fee)
|
||||
|
||||
case *lnwire.Error:
|
||||
// Error received from remote, MUST fail channel, but should
|
||||
// only print the contents of the error message if all
|
||||
@@ -2192,6 +2197,64 @@ func (l *channelLink) MayAddOutgoingHtlc() error {
|
||||
return l.channel.MayAddOutgoingHtlc()
|
||||
}
|
||||
|
||||
// getDustSum is a wrapper method that calls the underlying channel's dust sum
|
||||
// method.
|
||||
//
|
||||
// NOTE: Part of the dustHandler interface.
|
||||
func (l *channelLink) getDustSum(remote bool) lnwire.MilliSatoshi {
|
||||
return l.channel.GetDustSum(remote)
|
||||
}
|
||||
|
||||
// getFeeRate is a wrapper method that retrieves the underlying channel's
|
||||
// feerate.
|
||||
//
|
||||
// NOTE: Part of the dustHandler interface.
|
||||
func (l *channelLink) getFeeRate() chainfee.SatPerKWeight {
|
||||
return l.channel.CommitFeeRate()
|
||||
}
|
||||
|
||||
// getDustClosure returns a closure that can be used by the switch or mailbox
|
||||
// to evaluate whether a given HTLC is dust.
|
||||
//
|
||||
// NOTE: Part of the dustHandler interface.
|
||||
func (l *channelLink) getDustClosure() dustClosure {
|
||||
localDustLimit := l.channel.State().LocalChanCfg.DustLimit
|
||||
remoteDustLimit := l.channel.State().RemoteChanCfg.DustLimit
|
||||
chanType := l.channel.State().ChanType
|
||||
|
||||
return dustHelper(chanType, localDustLimit, remoteDustLimit)
|
||||
}
|
||||
|
||||
// dustClosure is a function that evaluates whether an HTLC is dust. It returns
|
||||
// true if the HTLC is dust. It takes in a feerate, a boolean denoting whether
|
||||
// the HTLC is incoming (i.e. one that the remote sent), a boolean denoting
|
||||
// whether to evaluate on the local or remote commit, and finally an HTLC
|
||||
// amount to test.
|
||||
type dustClosure func(chainfee.SatPerKWeight, bool, bool, btcutil.Amount) bool
|
||||
|
||||
// dustHelper is used to construct the dustClosure.
|
||||
func dustHelper(chantype channeldb.ChannelType, localDustLimit,
|
||||
remoteDustLimit btcutil.Amount) dustClosure {
|
||||
|
||||
isDust := func(feerate chainfee.SatPerKWeight, incoming,
|
||||
localCommit bool, amt btcutil.Amount) bool {
|
||||
|
||||
if localCommit {
|
||||
return lnwallet.HtlcIsDust(
|
||||
chantype, incoming, true, feerate, amt,
|
||||
localDustLimit,
|
||||
)
|
||||
}
|
||||
|
||||
return lnwallet.HtlcIsDust(
|
||||
chantype, incoming, false, feerate, amt,
|
||||
remoteDustLimit,
|
||||
)
|
||||
}
|
||||
|
||||
return isDust
|
||||
}
|
||||
|
||||
// AttachMailBox updates the current mailbox used by this link, and hooks up
|
||||
// the mailbox's message and packet outboxes to the link's upstream and
|
||||
// downstream chans, respectively.
|
||||
@@ -2201,6 +2264,14 @@ func (l *channelLink) AttachMailBox(mailbox MailBox) {
|
||||
l.upstream = mailbox.MessageOutBox()
|
||||
l.downstream = mailbox.PacketOutBox()
|
||||
l.Unlock()
|
||||
|
||||
// Set the mailbox's fee rate. This may be refreshing a feerate that was
|
||||
// never committed.
|
||||
l.mailBox.SetFeeRate(l.getFeeRate())
|
||||
|
||||
// Also set the mailbox's dust closure so that it can query whether HTLC's
|
||||
// are dust given the current feerate.
|
||||
l.mailBox.SetDustClosure(l.getDustClosure())
|
||||
}
|
||||
|
||||
// UpdateForwardingPolicy updates the forwarding policy for the target
|
||||
@@ -2501,6 +2572,10 @@ func (l *channelLink) updateChannelFee(feePerKw chainfee.SatPerKWeight) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// The fee passed the channel's validation checks, so we update the
|
||||
// mailbox feerate.
|
||||
l.mailBox.SetFeeRate(feePerKw)
|
||||
|
||||
// We'll then attempt to send a new UpdateFee message, and also lock it
|
||||
// in immediately by triggering a commitment update.
|
||||
msg := lnwire.NewUpdateFee(l.ChanID(), uint32(feePerKw))
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/lightningnetwork/lnd/clock"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
)
|
||||
|
||||
@@ -66,6 +67,17 @@ type MailBox interface {
|
||||
// Reset the packet head to point at the first element in the list.
|
||||
ResetPackets() error
|
||||
|
||||
// SetDustClosure takes in a closure that is used to evaluate whether
|
||||
// mailbox HTLC's are dust.
|
||||
SetDustClosure(isDust dustClosure)
|
||||
|
||||
// SetFeeRate sets the feerate to be used when evaluating dust.
|
||||
SetFeeRate(feerate chainfee.SatPerKWeight)
|
||||
|
||||
// DustPackets returns the dust sum for Adds in the mailbox for the
|
||||
// local and remote commitments.
|
||||
DustPackets() (lnwire.MilliSatoshi, lnwire.MilliSatoshi)
|
||||
|
||||
// Start starts the mailbox and any goroutines it needs to operate
|
||||
// properly.
|
||||
Start()
|
||||
@@ -131,6 +143,17 @@ type memoryMailBox struct {
|
||||
wireShutdown chan struct{}
|
||||
pktShutdown chan struct{}
|
||||
quit chan struct{}
|
||||
|
||||
// feeRate is set when the link receives or sends out fee updates. It
|
||||
// is refreshed when AttachMailBox is called in case a fee update did
|
||||
// not get committed. In some cases it may be out of sync with the
|
||||
// channel's feerate, but it should eventually get back in sync.
|
||||
feeRate chainfee.SatPerKWeight
|
||||
|
||||
// isDust is set when AttachMailBox is called and serves to evaluate
|
||||
// the outstanding dust in the memoryMailBox given the current set
|
||||
// feeRate.
|
||||
isDust dustClosure
|
||||
}
|
||||
|
||||
// newMemoryMailBox creates a new instance of the memoryMailBox.
|
||||
@@ -610,6 +633,61 @@ func (m *memoryMailBox) AddPacket(pkt *htlcPacket) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetFeeRate sets the memoryMailBox's feerate for use in DustPackets.
|
||||
func (m *memoryMailBox) SetFeeRate(feeRate chainfee.SatPerKWeight) {
|
||||
m.pktCond.L.Lock()
|
||||
defer m.pktCond.L.Unlock()
|
||||
|
||||
m.feeRate = feeRate
|
||||
}
|
||||
|
||||
// SetDustClosure sets the memoryMailBox's dustClosure for use in DustPackets.
|
||||
func (m *memoryMailBox) SetDustClosure(isDust dustClosure) {
|
||||
m.pktCond.L.Lock()
|
||||
defer m.pktCond.L.Unlock()
|
||||
|
||||
m.isDust = isDust
|
||||
}
|
||||
|
||||
// DustPackets returns the dust sum for add packets in the mailbox. The first
|
||||
// return value is the local dust sum and the second is the remote dust sum.
|
||||
// This will keep track of a given dust HTLC from the time it is added via
|
||||
// AddPacket until it is removed via AckPacket.
|
||||
func (m *memoryMailBox) DustPackets() (lnwire.MilliSatoshi,
|
||||
lnwire.MilliSatoshi) {
|
||||
|
||||
m.pktCond.L.Lock()
|
||||
defer m.pktCond.L.Unlock()
|
||||
|
||||
var (
|
||||
localDustSum lnwire.MilliSatoshi
|
||||
remoteDustSum lnwire.MilliSatoshi
|
||||
)
|
||||
|
||||
// Run through the map of HTLC's and determine the dust sum with calls
|
||||
// to the memoryMailBox's isDust closure. Note that all mailbox packets
|
||||
// are outgoing so the second argument to isDust will be false.
|
||||
for _, e := range m.addIndex {
|
||||
addPkt := e.Value.(*pktWithExpiry).pkt
|
||||
|
||||
// Evaluate whether this HTLC is dust on the local commitment.
|
||||
if m.isDust(
|
||||
m.feeRate, false, true, addPkt.amount.ToSatoshis(),
|
||||
) {
|
||||
localDustSum += addPkt.amount
|
||||
}
|
||||
|
||||
// Evaluate whether this HTLC is dust on the remote commitment.
|
||||
if m.isDust(
|
||||
m.feeRate, false, false, addPkt.amount.ToSatoshis(),
|
||||
) {
|
||||
remoteDustSum += addPkt.amount
|
||||
}
|
||||
}
|
||||
|
||||
return localDustSum, remoteDustSum
|
||||
}
|
||||
|
||||
// FailAdd fails an UpdateAddHTLC that exists within the mailbox, removing it
|
||||
// from the in-memory replay buffer. This will prevent the packet from being
|
||||
// delivered after the link restarts if the switch has remained online. The
|
||||
|
||||
@@ -6,9 +6,13 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/clock"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const testExpiry = time.Minute
|
||||
@@ -536,6 +540,126 @@ func TestMailBoxDuplicateAddPacket(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestMailBoxDustHandling tests that DustPackets returns the expected values
|
||||
// for the local and remote dust sum after calling SetFeeRate and
|
||||
// SetDustClosure.
|
||||
func TestMailBoxDustHandling(t *testing.T) {
|
||||
t.Run("tweakless mailbox dust", func(t *testing.T) {
|
||||
testMailBoxDust(t, channeldb.SingleFunderTweaklessBit)
|
||||
})
|
||||
t.Run("zero htlc fee anchors mailbox dust", func(t *testing.T) {
|
||||
testMailBoxDust(t, channeldb.SingleFunderTweaklessBit|
|
||||
channeldb.AnchorOutputsBit|
|
||||
channeldb.ZeroHtlcTxFeeBit,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func testMailBoxDust(t *testing.T, chantype channeldb.ChannelType) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := newMailboxContext(t, time.Now(), testExpiry)
|
||||
defer ctx.mailbox.Stop()
|
||||
|
||||
_, _, aliceID, bobID := genIDs()
|
||||
|
||||
// It should not be the case that the MailBox has packets before the
|
||||
// feeRate or dustClosure is set. This is because the mailbox is always
|
||||
// created *with* its associated link and attached via AttachMailbox,
|
||||
// where these parameters will be set. Even though the lifetime is
|
||||
// longer than the link, the setting will persist across multiple link
|
||||
// creations.
|
||||
ctx.mailbox.SetFeeRate(chainfee.SatPerKWeight(253))
|
||||
|
||||
localDustLimit := btcutil.Amount(400)
|
||||
remoteDustLimit := btcutil.Amount(500)
|
||||
isDust := dustHelper(chantype, localDustLimit, remoteDustLimit)
|
||||
ctx.mailbox.SetDustClosure(isDust)
|
||||
|
||||
// The first packet will be dust according to the remote dust limit,
|
||||
// but not the local. We set a different amount if this is a zero fee
|
||||
// htlc channel type.
|
||||
firstAmt := lnwire.MilliSatoshi(600_000)
|
||||
|
||||
if chantype.ZeroHtlcTxFee() {
|
||||
firstAmt = lnwire.MilliSatoshi(450_000)
|
||||
}
|
||||
|
||||
firstPkt := &htlcPacket{
|
||||
outgoingChanID: aliceID,
|
||||
outgoingHTLCID: 0,
|
||||
incomingChanID: bobID,
|
||||
incomingHTLCID: 0,
|
||||
amount: firstAmt,
|
||||
htlc: &lnwire.UpdateAddHTLC{
|
||||
ID: uint64(0),
|
||||
},
|
||||
}
|
||||
|
||||
err := ctx.mailbox.AddPacket(firstPkt)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Assert that the local sum is 0, and the remote sum accounts for this
|
||||
// added packet.
|
||||
localSum, remoteSum := ctx.mailbox.DustPackets()
|
||||
require.Equal(t, lnwire.MilliSatoshi(0), localSum)
|
||||
require.Equal(t, firstAmt, remoteSum)
|
||||
|
||||
// The next packet will be dust according to both limits.
|
||||
secondAmt := lnwire.MilliSatoshi(300_000)
|
||||
secondPkt := &htlcPacket{
|
||||
outgoingChanID: aliceID,
|
||||
outgoingHTLCID: 1,
|
||||
incomingChanID: bobID,
|
||||
incomingHTLCID: 1,
|
||||
amount: secondAmt,
|
||||
htlc: &lnwire.UpdateAddHTLC{
|
||||
ID: uint64(1),
|
||||
},
|
||||
}
|
||||
|
||||
err = ctx.mailbox.AddPacket(secondPkt)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Assert that both the local and remote sums have increased by the
|
||||
// second amount.
|
||||
localSum, remoteSum = ctx.mailbox.DustPackets()
|
||||
require.Equal(t, secondAmt, localSum)
|
||||
require.Equal(t, firstAmt+secondAmt, remoteSum)
|
||||
|
||||
// Now we pull both packets off of the queue.
|
||||
for i := 0; i < 2; i++ {
|
||||
select {
|
||||
case <-ctx.mailbox.PacketOutBox():
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
ctx.t.Fatalf("did not receive packet in time")
|
||||
}
|
||||
}
|
||||
|
||||
// Assert that the sums haven't changed.
|
||||
localSum, remoteSum = ctx.mailbox.DustPackets()
|
||||
require.Equal(t, secondAmt, localSum)
|
||||
require.Equal(t, firstAmt+secondAmt, remoteSum)
|
||||
|
||||
// Remove the first packet from the mailbox.
|
||||
removed := ctx.mailbox.AckPacket(firstPkt.inKey())
|
||||
require.True(t, removed)
|
||||
|
||||
// Assert that the remote sum does not include the firstAmt.
|
||||
localSum, remoteSum = ctx.mailbox.DustPackets()
|
||||
require.Equal(t, secondAmt, localSum)
|
||||
require.Equal(t, secondAmt, remoteSum)
|
||||
|
||||
// Remove the second packet from the mailbox.
|
||||
removed = ctx.mailbox.AckPacket(secondPkt.inKey())
|
||||
require.True(t, removed)
|
||||
|
||||
// Assert that both sums are equal to 0.
|
||||
localSum, remoteSum = ctx.mailbox.DustPackets()
|
||||
require.Equal(t, lnwire.MilliSatoshi(0), localSum)
|
||||
require.Equal(t, lnwire.MilliSatoshi(0), remoteSum)
|
||||
}
|
||||
|
||||
// TestMailOrchestrator asserts that the orchestrator properly buffers packets
|
||||
// for channels that haven't been made live, such that they are delivered
|
||||
// immediately after BindLiveShortChanID. It also tests that packets are delivered
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/go-errors/errors"
|
||||
sphinx "github.com/lightningnetwork/lightning-onion"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
@@ -188,6 +189,7 @@ func initSwitchWithDB(startingHeight uint32, db *channeldb.DB) (*Switch, error)
|
||||
HtlcNotifier: &mockHTLCNotifier{},
|
||||
Clock: clock.NewDefaultClock(),
|
||||
HTLCExpiry: time.Hour,
|
||||
DustThreshold: DefaultDustThreshold,
|
||||
}
|
||||
|
||||
return New(cfg, startingHeight)
|
||||
@@ -717,6 +719,21 @@ func (f *mockChannelLink) handleLocalAddPacket(pkt *htlcPacket) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *mockChannelLink) getDustSum(remote bool) lnwire.MilliSatoshi {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (f *mockChannelLink) getFeeRate() chainfee.SatPerKWeight {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (f *mockChannelLink) getDustClosure() dustClosure {
|
||||
dustLimit := btcutil.Amount(400)
|
||||
return dustHelper(
|
||||
channeldb.SingleFunderTweaklessBit, dustLimit, dustLimit,
|
||||
)
|
||||
}
|
||||
|
||||
func (f *mockChannelLink) HandleChannelUpdate(lnwire.Message) {
|
||||
}
|
||||
|
||||
@@ -742,6 +759,7 @@ func (f *mockChannelLink) Stats() (uint64, lnwire.MilliSatoshi, lnwire.MilliSato
|
||||
func (f *mockChannelLink) AttachMailBox(mailBox MailBox) {
|
||||
f.mailBox = mailBox
|
||||
f.packets = mailBox.PacketOutBox()
|
||||
mailBox.SetDustClosure(f.getDustClosure())
|
||||
}
|
||||
|
||||
func (f *mockChannelLink) Start() error {
|
||||
|
||||
+172
-21
@@ -71,6 +71,15 @@ var (
|
||||
// ErrLocalAddFailed signals that the ADD htlc for a local payment
|
||||
// failed to be processed.
|
||||
ErrLocalAddFailed = errors.New("local add HTLC failed")
|
||||
|
||||
// errDustThresholdExceeded is only surfaced to callers of SendHTLC and
|
||||
// signals that sending the HTLC would exceed the outgoing link's dust
|
||||
// threshold.
|
||||
errDustThresholdExceeded = errors.New("dust threshold exceeded")
|
||||
|
||||
// DefaultDustThreshold is the default threshold after which we'll fail
|
||||
// payments if they are dust. This is currently set to 500m msats.
|
||||
DefaultDustThreshold = lnwire.MilliSatoshi(500_000_000)
|
||||
)
|
||||
|
||||
// plexPacket encapsulates switch packet and adds error channel to receive
|
||||
@@ -178,6 +187,10 @@ type Config struct {
|
||||
// will expiry this long after the Adds are added to a mailbox via
|
||||
// AddPacket.
|
||||
HTLCExpiry time.Duration
|
||||
|
||||
// DustThreshold is the threshold in milli-satoshis after which we'll
|
||||
// fail incoming or outgoing dust payments for a particular channel.
|
||||
DustThreshold lnwire.MilliSatoshi
|
||||
}
|
||||
|
||||
// Switch is the central messaging bus for all incoming/outgoing HTLCs.
|
||||
@@ -455,6 +468,51 @@ func (s *Switch) SendHTLC(firstHop lnwire.ShortChannelID, attemptID uint64,
|
||||
htlc: htlc,
|
||||
}
|
||||
|
||||
// Attempt to fetch the target link before creating a circuit so that
|
||||
// we don't leave dangling circuits. The getLocalLink method does not
|
||||
// require the circuit variable to be set on the *htlcPacket.
|
||||
link, linkErr := s.getLocalLink(packet, htlc)
|
||||
if linkErr != nil {
|
||||
// Notify the htlc notifier of a link failure on our outgoing
|
||||
// link. Incoming timelock/amount values are not set because
|
||||
// they are not present for local sends.
|
||||
s.cfg.HtlcNotifier.NotifyLinkFailEvent(
|
||||
newHtlcKey(packet),
|
||||
HtlcInfo{
|
||||
OutgoingTimeLock: htlc.Expiry,
|
||||
OutgoingAmt: htlc.Amount,
|
||||
},
|
||||
HtlcEventTypeSend,
|
||||
linkErr,
|
||||
false,
|
||||
)
|
||||
|
||||
return linkErr
|
||||
}
|
||||
|
||||
// Evaluate whether this HTLC would increase our exposure to dust. If
|
||||
// it does, don't send it out and instead return an error.
|
||||
if s.evaluateDustThreshold(link, htlc.Amount, false) {
|
||||
// Notify the htlc notifier of a link failure on our outgoing
|
||||
// link. We use the FailTemporaryChannelFailure in place of a
|
||||
// more descriptive error message.
|
||||
linkErr := NewLinkError(
|
||||
&lnwire.FailTemporaryChannelFailure{},
|
||||
)
|
||||
s.cfg.HtlcNotifier.NotifyLinkFailEvent(
|
||||
newHtlcKey(packet),
|
||||
HtlcInfo{
|
||||
OutgoingTimeLock: htlc.Expiry,
|
||||
OutgoingAmt: htlc.Amount,
|
||||
},
|
||||
HtlcEventTypeSend,
|
||||
linkErr,
|
||||
false,
|
||||
)
|
||||
|
||||
return errDustThresholdExceeded
|
||||
}
|
||||
|
||||
circuit := newPaymentCircuit(&htlc.PaymentHash, packet)
|
||||
actions, err := s.circuits.CommitCircuits(circuit)
|
||||
if err != nil {
|
||||
@@ -474,27 +532,6 @@ func (s *Switch) SendHTLC(firstHop lnwire.ShortChannelID, attemptID uint64,
|
||||
// Send packet to link.
|
||||
packet.circuit = circuit
|
||||
|
||||
// User has created the htlc update therefore we should find the
|
||||
// appropriate channel link and send the payment over this link.
|
||||
link, linkErr := s.getLocalLink(packet, htlc)
|
||||
if linkErr != nil {
|
||||
// Notify the htlc notifier of a link failure on our
|
||||
// outgoing link. Incoming timelock/amount values are
|
||||
// not set because they are not present for local sends.
|
||||
s.cfg.HtlcNotifier.NotifyLinkFailEvent(
|
||||
newHtlcKey(packet),
|
||||
HtlcInfo{
|
||||
OutgoingTimeLock: htlc.Expiry,
|
||||
OutgoingAmt: htlc.Amount,
|
||||
},
|
||||
HtlcEventTypeSend,
|
||||
linkErr,
|
||||
false,
|
||||
)
|
||||
|
||||
return linkErr
|
||||
}
|
||||
|
||||
return link.handleLocalAddPacket(packet)
|
||||
}
|
||||
|
||||
@@ -1084,6 +1121,50 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error {
|
||||
// what the best channel is.
|
||||
destination := destinations[rand.Intn(len(destinations))]
|
||||
|
||||
// Retrieve the incoming link by its ShortChannelID. Note that
|
||||
// the incomingChanID is never set to hop.Source here.
|
||||
s.indexMtx.RLock()
|
||||
incomingLink, err := s.getLinkByShortID(packet.incomingChanID)
|
||||
s.indexMtx.RUnlock()
|
||||
if err != nil {
|
||||
// If we couldn't find the incoming link, we can't
|
||||
// evaluate the incoming's exposure to dust, so we just
|
||||
// fail the HTLC back.
|
||||
linkErr := NewLinkError(
|
||||
&lnwire.FailTemporaryChannelFailure{},
|
||||
)
|
||||
|
||||
return s.failAddPacket(packet, linkErr)
|
||||
}
|
||||
|
||||
// Evaluate whether this HTLC would increase our exposure to
|
||||
// dust on the incoming link. If it does, fail it backwards.
|
||||
if s.evaluateDustThreshold(
|
||||
incomingLink, packet.incomingAmount, true,
|
||||
) {
|
||||
// The incoming dust exceeds the threshold, so we fail
|
||||
// the add back.
|
||||
linkErr := NewLinkError(
|
||||
&lnwire.FailTemporaryChannelFailure{},
|
||||
)
|
||||
|
||||
return s.failAddPacket(packet, linkErr)
|
||||
}
|
||||
|
||||
// Also evaluate whether this HTLC would increase our exposure
|
||||
// to dust on the destination link. If it does, fail it back.
|
||||
if s.evaluateDustThreshold(
|
||||
destination, packet.amount, false,
|
||||
) {
|
||||
// The outgoing dust exceeds the threshold, so we fail
|
||||
// the add back.
|
||||
linkErr := NewLinkError(
|
||||
&lnwire.FailTemporaryChannelFailure{},
|
||||
)
|
||||
|
||||
return s.failAddPacket(packet, linkErr)
|
||||
}
|
||||
|
||||
// Send the packet to the destination channel link which
|
||||
// manages the channel.
|
||||
packet.outgoingChanID = destination.ShortChanID()
|
||||
@@ -2254,3 +2335,73 @@ func (s *Switch) FlushForwardingEvents() error {
|
||||
func (s *Switch) BestHeight() uint32 {
|
||||
return atomic.LoadUint32(&s.bestHeight)
|
||||
}
|
||||
|
||||
// evaluateDustThreshold takes in a ChannelLink, HTLC amount, and a boolean to
|
||||
// determine whether the default dust threshold has been exceeded. This
|
||||
// heuristic takes into account the trimmed-to-dust mechanism. The sum of the
|
||||
// commitment's dust with the mailbox's dust with the amount is checked against
|
||||
// the default threshold. If incoming is true, then the amount is not included
|
||||
// in the sum as it was already included in the commitment's dust. A boolean is
|
||||
// returned telling the caller whether the HTLC should be failed back.
|
||||
func (s *Switch) evaluateDustThreshold(link ChannelLink,
|
||||
amount lnwire.MilliSatoshi, incoming bool) bool {
|
||||
|
||||
// Retrieve the link's current commitment feerate and dustClosure.
|
||||
feeRate := link.getFeeRate()
|
||||
isDust := link.getDustClosure()
|
||||
|
||||
// Evaluate if the HTLC is dust on either sides' commitment.
|
||||
isLocalDust := isDust(feeRate, incoming, true, amount.ToSatoshis())
|
||||
isRemoteDust := isDust(feeRate, incoming, false, amount.ToSatoshis())
|
||||
|
||||
if !(isLocalDust || isRemoteDust) {
|
||||
// If the HTLC is not dust on either commitment, it's fine to
|
||||
// forward.
|
||||
return false
|
||||
}
|
||||
|
||||
// Fetch the dust sums currently in the mailbox for this link.
|
||||
cid := link.ChanID()
|
||||
sid := link.ShortChanID()
|
||||
mailbox := s.mailOrchestrator.GetOrCreateMailBox(cid, sid)
|
||||
localMailDust, remoteMailDust := mailbox.DustPackets()
|
||||
|
||||
// If the htlc is dust on the local commitment, we'll obtain the dust
|
||||
// sum for it.
|
||||
if isLocalDust {
|
||||
localSum := link.getDustSum(false)
|
||||
localSum += localMailDust
|
||||
|
||||
// Optionally include the HTLC amount only for outgoing
|
||||
// HTLCs.
|
||||
if !incoming {
|
||||
localSum += amount
|
||||
}
|
||||
|
||||
// Finally check against the defined dust threshold.
|
||||
if localSum > s.cfg.DustThreshold {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Also check if the htlc is dust on the remote commitment, if we've
|
||||
// reached this point.
|
||||
if isRemoteDust {
|
||||
remoteSum := link.getDustSum(true)
|
||||
remoteSum += remoteMailDust
|
||||
|
||||
// Optionally include the HTLC amount only for outgoing
|
||||
// HTLCs.
|
||||
if !incoming {
|
||||
remoteSum += amount
|
||||
}
|
||||
|
||||
// Finally check against the defined dust threshold.
|
||||
if remoteSum > s.cfg.DustThreshold {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// If we reached this point, this HTLC is fine to forward.
|
||||
return false
|
||||
}
|
||||
@@ -13,10 +13,12 @@ import (
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/htlcswitch/hodl"
|
||||
"github.com/lightningnetwork/lnd/htlcswitch/hop"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/ticker"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var zeroCircuit = channeldb.CircuitKey{}
|
||||
@@ -3318,3 +3320,363 @@ func TestSwitchHoldForward(t *testing.T) {
|
||||
assertOutgoingLinkReceive(t, aliceChannelLink, true)
|
||||
assertNumCircuits(t, s, 0, 0)
|
||||
}
|
||||
|
||||
// TestSwitchDustForwarding tests that the switch properly fails HTLC's which
|
||||
// have incoming or outgoing links that breach their dust thresholds.
|
||||
func TestSwitchDustForwarding(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// We'll create a three-hop network:
|
||||
// - Alice has a dust limit of 200sats with Bob
|
||||
// - Bob has a dust limit of 800sats with Alice
|
||||
// - Bob has a dust limit of 200sats with Carol
|
||||
// - Carol has a dust limit of 800sats with Bob
|
||||
channels, cleanUp, _, err := createClusterChannels(
|
||||
btcutil.SatoshiPerBitcoin, btcutil.SatoshiPerBitcoin,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer cleanUp()
|
||||
|
||||
n := newThreeHopNetwork(
|
||||
t, channels.aliceToBob, channels.bobToAlice,
|
||||
channels.bobToCarol, channels.carolToBob, testStartingHeight,
|
||||
)
|
||||
err = n.start()
|
||||
require.NoError(t, err)
|
||||
|
||||
// We'll also put Alice and Bob into hodl.ExitSettle mode, such that
|
||||
// they won't settle incoming exit-hop HTLC's automatically.
|
||||
n.aliceChannelLink.cfg.HodlMask = hodl.ExitSettle.Mask()
|
||||
n.firstBobChannelLink.cfg.HodlMask = hodl.ExitSettle.Mask()
|
||||
|
||||
// We'll test that once the default threshold is exceeded on the
|
||||
// Alice -> Bob channel, either side's calls to SendHTLC will fail.
|
||||
// This does not rely on the mailbox sum since there's no intermediate
|
||||
// hop.
|
||||
//
|
||||
// Alice will send 357 HTLC's of 700sats. Bob will also send 357 HTLC's
|
||||
// of 700sats. If either side attempts to send a dust HTLC, it will
|
||||
// fail so amounts below 800sats will breach the dust threshold.
|
||||
amt := lnwire.NewMSatFromSatoshis(700)
|
||||
aliceBobFirstHop := n.aliceChannelLink.ShortChanID()
|
||||
|
||||
sendDustHtlcs(t, n, true, amt, aliceBobFirstHop)
|
||||
sendDustHtlcs(t, n, false, amt, aliceBobFirstHop)
|
||||
|
||||
// Generate the parameters needed for Bob to send another dust HTLC.
|
||||
_, timelock, hops := generateHops(
|
||||
amt, testStartingHeight, n.aliceChannelLink,
|
||||
)
|
||||
|
||||
blob, err := generateRoute(hops...)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Assert that if Bob sends a dust HTLC it will fail.
|
||||
failingPreimage := lntypes.Preimage{0, 0, 3}
|
||||
failingHash := failingPreimage.Hash()
|
||||
failingHtlc := &lnwire.UpdateAddHTLC{
|
||||
PaymentHash: failingHash,
|
||||
Amount: amt,
|
||||
Expiry: timelock,
|
||||
OnionBlob: blob,
|
||||
}
|
||||
|
||||
// Assert that the HTLC is failed due to the dust threshold.
|
||||
err = n.bobServer.htlcSwitch.SendHTLC(
|
||||
aliceBobFirstHop, uint64(357), failingHtlc,
|
||||
)
|
||||
require.ErrorIs(t, err, errDustThresholdExceeded)
|
||||
|
||||
// Generate the parameters needed for bob to send a non-dust HTLC.
|
||||
nondustAmt := lnwire.NewMSatFromSatoshis(10_000)
|
||||
_, _, hops = generateHops(
|
||||
nondustAmt, testStartingHeight, n.aliceChannelLink,
|
||||
)
|
||||
|
||||
blob, err = generateRoute(hops...)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Now attempt to send an HTLC above Bob's dust limit. It should
|
||||
// succeed.
|
||||
nondustPreimage := lntypes.Preimage{0, 0, 4}
|
||||
nondustHash := nondustPreimage.Hash()
|
||||
nondustHtlc := &lnwire.UpdateAddHTLC{
|
||||
PaymentHash: nondustHash,
|
||||
Amount: nondustAmt,
|
||||
Expiry: timelock,
|
||||
OnionBlob: blob,
|
||||
}
|
||||
|
||||
// Assert that SendHTLC succeeds and evaluateDustThreshold returns
|
||||
// false.
|
||||
err = n.bobServer.htlcSwitch.SendHTLC(
|
||||
aliceBobFirstHop, uint64(358), nondustHtlc,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Introduce Carol into the mix and assert that sending a multi-hop
|
||||
// dust HTLC to Alice will fail. Bob should fail back the HTLC with a
|
||||
// temporary channel failure.
|
||||
carolAmt, carolTimelock, carolHops := generateHops(
|
||||
amt, testStartingHeight, n.secondBobChannelLink,
|
||||
n.aliceChannelLink,
|
||||
)
|
||||
|
||||
carolBlob, err := generateRoute(carolHops...)
|
||||
require.NoError(t, err)
|
||||
|
||||
carolPreimage := lntypes.Preimage{0, 0, 5}
|
||||
carolHash := carolPreimage.Hash()
|
||||
carolHtlc := &lnwire.UpdateAddHTLC{
|
||||
PaymentHash: carolHash,
|
||||
Amount: carolAmt,
|
||||
Expiry: carolTimelock,
|
||||
OnionBlob: carolBlob,
|
||||
}
|
||||
|
||||
// Initialize Carol's attempt ID.
|
||||
carolAttemptID := 0
|
||||
|
||||
err = n.carolServer.htlcSwitch.SendHTLC(
|
||||
n.carolChannelLink.ShortChanID(), uint64(carolAttemptID),
|
||||
carolHtlc,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
carolAttemptID++
|
||||
|
||||
carolResultChan, err := n.carolServer.htlcSwitch.GetPaymentResult(
|
||||
uint64(carolAttemptID-1), carolHash, newMockDeobfuscator(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case result, ok := <-carolResultChan:
|
||||
require.True(t, ok)
|
||||
assertFailureCode(
|
||||
t, result.Error, lnwire.CodeTemporaryChannelFailure,
|
||||
)
|
||||
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("no result arrived for carol's dust htlc")
|
||||
}
|
||||
|
||||
// Send an HTLC from Alice to Carol and assert that it is failed at the
|
||||
// call to SendHTLC.
|
||||
htlcAmt, totalTimelock, aliceHops := generateHops(
|
||||
amt, testStartingHeight, n.firstBobChannelLink,
|
||||
n.carolChannelLink,
|
||||
)
|
||||
|
||||
blob, err = generateRoute(aliceHops...)
|
||||
require.NoError(t, err)
|
||||
|
||||
aliceMultihopPreimage := lntypes.Preimage{0, 0, 6}
|
||||
aliceMultihopHash := aliceMultihopPreimage.Hash()
|
||||
aliceMultihopHtlc := &lnwire.UpdateAddHTLC{
|
||||
PaymentHash: aliceMultihopHash,
|
||||
Amount: htlcAmt,
|
||||
Expiry: totalTimelock,
|
||||
OnionBlob: blob,
|
||||
}
|
||||
|
||||
err = n.aliceServer.htlcSwitch.SendHTLC(
|
||||
n.aliceChannelLink.ShortChanID(), uint64(357),
|
||||
aliceMultihopHtlc,
|
||||
)
|
||||
require.ErrorIs(t, err, errDustThresholdExceeded)
|
||||
}
|
||||
|
||||
// sendDustHtlcs is a helper function used to send many dust HTLC's to test the
|
||||
// Switch's dust-threshold logic. It takes a boolean denoting whether or not
|
||||
// Alice is the sender.
|
||||
func sendDustHtlcs(t *testing.T, n *threeHopNetwork, alice bool,
|
||||
amt lnwire.MilliSatoshi, sid lnwire.ShortChannelID) {
|
||||
|
||||
t.Helper()
|
||||
|
||||
// The number of dust HTLC's we'll send for both Alice and Bob.
|
||||
numHTLCs := 357
|
||||
|
||||
// Extract the destination into a variable. If alice is the sender, the
|
||||
// destination is Bob.
|
||||
destLink := n.aliceChannelLink
|
||||
if alice {
|
||||
destLink = n.firstBobChannelLink
|
||||
}
|
||||
|
||||
// Create hops that will be used in the onion payload.
|
||||
htlcAmt, totalTimelock, hops := generateHops(
|
||||
amt, testStartingHeight, destLink,
|
||||
)
|
||||
|
||||
// Convert the hops to a blob that will be put in the Add message.
|
||||
blob, err := generateRoute(hops...)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a slice to store the preimages.
|
||||
preimages := make([]lntypes.Preimage, numHTLCs)
|
||||
|
||||
// Initialize the attempt ID used in SendHTLC calls.
|
||||
attemptID := uint64(0)
|
||||
|
||||
// Deterministically generate preimages. Avoid the all-zeroes preimage
|
||||
// because that will be rejected by the database. We'll use a different
|
||||
// third byte for Alice and Bob.
|
||||
endByte := byte(2)
|
||||
if alice {
|
||||
endByte = byte(3)
|
||||
}
|
||||
|
||||
for i := 0; i < numHTLCs; i++ {
|
||||
preimages[i] = lntypes.Preimage{byte(i >> 8), byte(i), endByte}
|
||||
}
|
||||
|
||||
sendingSwitch := n.bobServer.htlcSwitch
|
||||
if alice {
|
||||
sendingSwitch = n.aliceServer.htlcSwitch
|
||||
}
|
||||
|
||||
// Call SendHTLC in a loop for numHTLCs.
|
||||
for i := 0; i < numHTLCs; i++ {
|
||||
// Construct the htlc packet.
|
||||
hash := preimages[i].Hash()
|
||||
|
||||
htlc := &lnwire.UpdateAddHTLC{
|
||||
PaymentHash: hash,
|
||||
Amount: htlcAmt,
|
||||
Expiry: totalTimelock,
|
||||
OnionBlob: blob,
|
||||
}
|
||||
|
||||
err = sendingSwitch.SendHTLC(sid, attemptID, htlc)
|
||||
require.NoError(t, err)
|
||||
attemptID++
|
||||
}
|
||||
}
|
||||
|
||||
// TestSwitchMailboxDust tests that the switch takes into account the mailbox
|
||||
// dust when evaluating the dust threshold. The mockChannelLink does not have
|
||||
// channel state, so this only tests the switch-mailbox interaction.
|
||||
func TestSwitchMailboxDust(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
alicePeer, err := newMockServer(
|
||||
t, "alice", testStartingHeight, nil, testDefaultDelta,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
bobPeer, err := newMockServer(
|
||||
t, "bob", testStartingHeight, nil, testDefaultDelta,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
carolPeer, err := newMockServer(
|
||||
t, "carol", testStartingHeight, nil, testDefaultDelta,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
s, err := initSwitchWithDB(testStartingHeight, nil)
|
||||
require.NoError(t, err)
|
||||
err = s.Start()
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_ = s.Stop()
|
||||
}()
|
||||
|
||||
chanID1, chanID2, aliceChanID, bobChanID := genIDs()
|
||||
|
||||
chanID3, carolChanID := genID()
|
||||
|
||||
aliceLink := newMockChannelLink(
|
||||
s, chanID1, aliceChanID, alicePeer, true,
|
||||
)
|
||||
err = s.AddLink(aliceLink)
|
||||
require.NoError(t, err)
|
||||
|
||||
bobLink := newMockChannelLink(
|
||||
s, chanID2, bobChanID, bobPeer, true,
|
||||
)
|
||||
err = s.AddLink(bobLink)
|
||||
require.NoError(t, err)
|
||||
|
||||
carolLink := newMockChannelLink(
|
||||
s, chanID3, carolChanID, carolPeer, true,
|
||||
)
|
||||
err = s.AddLink(carolLink)
|
||||
require.NoError(t, err)
|
||||
|
||||
// mockChannelLink sets the local and remote dust limits of the mailbox
|
||||
// to 400 satoshis and the feerate to 0. We'll fill the mailbox up with
|
||||
// dust packets and assert that calls to SendHTLC will fail.
|
||||
preimage, err := genPreimage()
|
||||
require.NoError(t, err)
|
||||
rhash := sha256.Sum256(preimage[:])
|
||||
amt := lnwire.NewMSatFromSatoshis(350)
|
||||
addMsg := &lnwire.UpdateAddHTLC{
|
||||
PaymentHash: rhash,
|
||||
Amount: amt,
|
||||
ChanID: chanID1,
|
||||
}
|
||||
|
||||
// Initialize the carolHTLCID.
|
||||
var carolHTLCID uint64
|
||||
|
||||
// It will take aliceCount HTLC's of 350sats to fill up Alice's mailbox
|
||||
// to the point where another would put Alice over the dust threshold.
|
||||
aliceCount := 1428
|
||||
|
||||
mailbox := s.mailOrchestrator.GetOrCreateMailBox(chanID1, aliceChanID)
|
||||
|
||||
for i := 0; i < aliceCount; i++ {
|
||||
alicePkt := &htlcPacket{
|
||||
incomingChanID: carolChanID,
|
||||
incomingHTLCID: carolHTLCID,
|
||||
outgoingChanID: aliceChanID,
|
||||
obfuscator: NewMockObfuscator(),
|
||||
incomingAmount: amt,
|
||||
amount: amt,
|
||||
htlc: addMsg,
|
||||
}
|
||||
|
||||
err = mailbox.AddPacket(alicePkt)
|
||||
require.NoError(t, err)
|
||||
|
||||
carolHTLCID++
|
||||
}
|
||||
|
||||
// Sending one more HTLC to Alice should result in the dust threshold
|
||||
// being breached.
|
||||
err = s.SendHTLC(aliceChanID, 0, addMsg)
|
||||
require.ErrorIs(t, err, errDustThresholdExceeded)
|
||||
|
||||
// We'll now call ForwardPackets from Bob to ensure that the mailbox
|
||||
// sum is also accounted for in the forwarding case.
|
||||
packet := &htlcPacket{
|
||||
incomingChanID: bobChanID,
|
||||
incomingHTLCID: 0,
|
||||
outgoingChanID: aliceChanID,
|
||||
obfuscator: NewMockObfuscator(),
|
||||
incomingAmount: amt,
|
||||
amount: amt,
|
||||
htlc: &lnwire.UpdateAddHTLC{
|
||||
PaymentHash: rhash,
|
||||
Amount: amt,
|
||||
ChanID: chanID1,
|
||||
},
|
||||
}
|
||||
|
||||
err = s.ForwardPackets(nil, packet)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Bob should receive a failure from the switch.
|
||||
select {
|
||||
case p := <-bobLink.packets:
|
||||
require.NotEmpty(t, p.linkFailure)
|
||||
assertFailureCode(
|
||||
t, p.linkFailure, lnwire.CodeTemporaryChannelFailure,
|
||||
)
|
||||
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("no timely reply from switch")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user