Merge pull request #5770 from Crypt-iQ/dust_threshold_0619

lnwallet+htlcswitch: make Switch dust-aware
This commit is contained in:
Olaoluwa Osuntokun authored and GitHub committed 2021-09-30 20:20:19 -07:00
commit 32fa48df7d
16 files changed
+1120 -39

No files matched your search

+19
View File
@@ -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
+75
View File
@@ -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))
+78
View File
@@ -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
+124
View File
@@ -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
+18
View File
@@ -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
View File
@@ -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
}
+362
View File
@@ -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")
}
}