Merge pull request #5770 from Crypt-iQ/dust_threshold_0619

lnwallet+htlcswitch: make Switch dust-aware
This commit is contained in:
Olaoluwa Osuntokun 2021-09-30 20:20:19 -07:00 committed by GitHub
commit 32fa48df7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1120 additions and 39 deletions

View File

@ -355,6 +355,8 @@ type Config struct {
GcCanceledInvoicesOnTheFly bool `long:"gc-canceled-invoices-on-the-fly" description:"If true, we'll delete newly canceled invoices on the fly."`
DustThreshold uint64 `long:"dust-threshold" description:"Sets the dust sum threshold in satoshis for a channel after which dust HTLC's will be failed."`
Invoices *lncfg.Invoices `group:"invoices" namespace:"invoices"`
Routing *lncfg.Routing `group:"routing" namespace:"routing"`
@ -550,6 +552,7 @@ func DefaultConfig() Config {
MaxOutgoingCltvExpiry: htlcswitch.DefaultMaxOutgoingCltvExpiry,
MaxChannelFeeAllocation: htlcswitch.DefaultMaxLinkFeeAllocation,
MaxCommitFeeRateAnchors: lnwallet.DefaultAnchorsCommitMaxFeeRateSatPerVByte,
DustThreshold: uint64(htlcswitch.DefaultDustThreshold.ToSatoshis()),
LogWriter: build.NewRotatingLogWriter(),
DB: lncfg.DefaultDB(),
Cluster: lncfg.DefaultCluster(),

View File

@ -4,6 +4,10 @@
* [The `DefaultDustLimit` method has been removed in favor of `DustLimitForSize` which calculates the proper network dust limit for a given output size. This also fixes certain APIs like SendCoins to be able to send 294 sats to a P2WPKH script.](https://github.com/lightningnetwork/lnd/pull/5781)
## Safety
* [The `htlcswitch.Switch` has been modified to take into account the total dust sum on the incoming and outgoing channels before forwarding. After the defined threshold is reached, dust HTLC's will start to be failed. The default threshold is 500K satoshis and can be modified by setting `--dust-threshold=` when running `lnd`.](https://github.com/lightningnetwork/lnd/pull/5770)
# Contributors (Alphabetical Order)
* Eugene Siegel

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

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))

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

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

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 {

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
}

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")
}
}

View File

@ -203,6 +203,7 @@ func TestLightningNetworkDaemon(t *testing.T) {
// TODO(roasbeef): create master balanced channel with all the monies?
aliceBobArgs := []string{
"--default-remote-max-htlcs=483",
"--dust-threshold=5000000",
}
// Run the subset of the test cases selected in this tranche.

View File

@ -615,7 +615,7 @@ func (c *commitment) populateHtlcIndexes(chanType channeldb.ChannelType,
// populateIndex is a helper function that populates the necessary
// indexes within the commitment view for a particular HTLC.
populateIndex := func(htlc *PaymentDescriptor, incoming bool) error {
isDust := htlcIsDust(
isDust := HtlcIsDust(
chanType, incoming, c.isOurs, c.feePerKw,
htlc.Amount.ToSatoshis(), c.dustLimit,
)
@ -792,7 +792,7 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight,
// generate them in order to locate the outputs within the commitment
// transaction. As we'll mark dust with a special output index in the
// on-disk state snapshot.
isDustLocal := htlcIsDust(
isDustLocal := HtlcIsDust(
chanType, htlc.Incoming, true, feeRate,
htlc.Amt.ToSatoshis(), lc.channelState.LocalChanCfg.DustLimit,
)
@ -805,7 +805,7 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight,
return pd, err
}
}
isDustRemote := htlcIsDust(
isDustRemote := HtlcIsDust(
chanType, htlc.Incoming, false, feeRate,
htlc.Amt.ToSatoshis(), lc.channelState.RemoteChanCfg.DustLimit,
)
@ -1436,7 +1436,7 @@ func (lc *LightningChannel) logUpdateToPayDesc(logUpdate *channeldb.LogUpdate,
pd.OnionBlob = make([]byte, len(wireMsg.OnionBlob))
copy(pd.OnionBlob[:], wireMsg.OnionBlob[:])
isDustRemote := htlcIsDust(
isDustRemote := HtlcIsDust(
lc.channelState.ChanType, false, false, feeRate,
wireMsg.Amount.ToSatoshis(), remoteDustLimit,
)
@ -2401,7 +2401,7 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64,
for _, htlc := range revokedSnapshot.Htlcs {
// If the HTLC is dust, then we'll skip it as it doesn't have
// an output on the commitment transaction.
if htlcIsDust(
if HtlcIsDust(
chanState.ChanType, htlc.Incoming, false,
chainfee.SatPerKWeight(revokedSnapshot.FeePerKw),
htlc.Amt.ToSatoshis(), chanState.RemoteChanCfg.DustLimit,
@ -2473,13 +2473,13 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64,
}, nil
}
// htlcIsDust determines if an HTLC output is dust or not depending on two
// HtlcIsDust determines if an HTLC output is dust or not depending on two
// bits: if the HTLC is incoming and if the HTLC will be placed on our
// commitment transaction, or theirs. These two pieces of information are
// require as we currently used second-level HTLC transactions as off-chain
// covenants. Depending on the two bits, we'll either be using a timeout or
// success transaction which have different weights.
func htlcIsDust(chanType channeldb.ChannelType,
func HtlcIsDust(chanType channeldb.ChannelType,
incoming, ourCommit bool, feePerKw chainfee.SatPerKWeight,
htlcAmt, dustLimit btcutil.Amount) bool {
@ -2995,7 +2995,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
// dust output after taking into account second-level HTLC fees, then a
// sigJob will be generated and appended to the current batch.
for _, htlc := range remoteCommitView.incomingHTLCs {
if htlcIsDust(
if HtlcIsDust(
chanType, true, false, feePerKw,
htlc.Amount.ToSatoshis(), dustLimit,
) {
@ -3049,7 +3049,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
sigBatch = append(sigBatch, sigJob)
}
for _, htlc := range remoteCommitView.outgoingHTLCs {
if htlcIsDust(
if HtlcIsDust(
chanType, false, false, feePerKw,
htlc.Amount.ToSatoshis(), dustLimit,
) {
@ -4040,7 +4040,7 @@ func (lc *LightningChannel) computeView(view *htlcView, remoteChain bool,
// weight, needed to calculate the transaction fee.
var totalHtlcWeight int64
for _, htlc := range filteredHTLCView.ourUpdates {
if htlcIsDust(
if HtlcIsDust(
lc.channelState.ChanType, false, !remoteChain,
feePerKw, htlc.Amount.ToSatoshis(), dustLimit,
) {
@ -4050,7 +4050,7 @@ func (lc *LightningChannel) computeView(view *htlcView, remoteChain bool,
totalHtlcWeight += input.HTLCWeight
}
for _, htlc := range filteredHTLCView.theirUpdates {
if htlcIsDust(
if HtlcIsDust(
lc.channelState.ChanType, true, !remoteChain,
feePerKw, htlc.Amount.ToSatoshis(), dustLimit,
) {
@ -4976,6 +4976,67 @@ func (lc *LightningChannel) AddHTLC(htlc *lnwire.UpdateAddHTLC,
return pd.HtlcIndex, nil
}
// GetDustSum takes in a boolean that determines which commitment to evaluate
// the dust sum on. The return value is the sum of dust on the desired
// commitment tx.
//
// NOTE: This over-estimates the dust exposure.
func (lc *LightningChannel) GetDustSum(remote bool) lnwire.MilliSatoshi {
lc.RLock()
defer lc.RUnlock()
var dustSum lnwire.MilliSatoshi
dustLimit := lc.channelState.LocalChanCfg.DustLimit
commit := lc.channelState.LocalCommitment
if remote {
// Calculate dust sum on the remote's commitment.
dustLimit = lc.channelState.RemoteChanCfg.DustLimit
commit = lc.channelState.RemoteCommitment
}
chanType := lc.channelState.ChanType
feeRate := chainfee.SatPerKWeight(commit.FeePerKw)
// Grab all of our HTLCs and evaluate against the dust limit.
for e := lc.localUpdateLog.Front(); e != nil; e = e.Next() {
pd := e.Value.(*PaymentDescriptor)
if pd.EntryType != Add {
continue
}
amt := pd.Amount.ToSatoshis()
// If the satoshi amount is under the dust limit, add the msat
// amount to the dust sum.
if HtlcIsDust(
chanType, false, !remote, feeRate, amt, dustLimit,
) {
dustSum += pd.Amount
}
}
// Grab all of their HTLCs and evaluate against the dust limit.
for e := lc.remoteUpdateLog.Front(); e != nil; e = e.Next() {
pd := e.Value.(*PaymentDescriptor)
if pd.EntryType != Add {
continue
}
amt := pd.Amount.ToSatoshis()
// If the satoshi amount is under the dust limit, add the msat
// amount to the dust sum.
if HtlcIsDust(
chanType, true, !remote, feeRate, amt, dustLimit,
) {
dustSum += pd.Amount
}
}
return dustSum
}
// MayAddOutgoingHtlc validates whether we can add an outgoing htlc to this
// channel. We don't have a value or circuit for this htlc, because we just
// want to test that we have slots for a potential htlc so we use a "mock"
@ -6056,7 +6117,7 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool,
// We'll skip any HTLC's which were dust on the commitment
// transaction, as these don't have a corresponding output
// within the commitment transaction.
if htlcIsDust(
if HtlcIsDust(
chanType, htlc.Incoming, ourCommit, feePerKw,
htlc.Amt.ToSatoshis(), dustLimit,
) {

View File

@ -9816,3 +9816,178 @@ func assertCleanOrDirty(clean bool, alice, bob *LightningChannel,
require.False(t, alice.IsChannelClean())
require.False(t, bob.IsChannelClean())
}
// TestChannelGetDustSum tests that we correctly calculate the channel's dust
// sum for the local and remote commitments.
func TestChannelGetDustSum(t *testing.T) {
t.Run("dust sum tweakless", func(t *testing.T) {
testGetDustSum(t, channeldb.SingleFunderTweaklessBit)
})
t.Run("dust sum anchors zero htlc fee", func(t *testing.T) {
testGetDustSum(t, channeldb.SingleFunderTweaklessBit|
channeldb.AnchorOutputsBit|
channeldb.ZeroHtlcTxFeeBit,
)
})
}
func testGetDustSum(t *testing.T, chantype channeldb.ChannelType) {
t.Parallel()
// This makes a channel with Alice's dust limit set to 200sats and
// Bob's dust limit set to 1300sats.
aliceChannel, bobChannel, cleanUp, err := CreateTestChannels(chantype)
require.NoError(t, err)
defer cleanUp()
// Use a function closure to assert the dust sum for a passed channel's
// local and remote commitments match the expected values.
checkDust := func(c *LightningChannel, expLocal,
expRemote lnwire.MilliSatoshi) {
localDustSum := c.GetDustSum(false)
require.Equal(t, expLocal, localDustSum)
remoteDustSum := c.GetDustSum(true)
require.Equal(t, expRemote, remoteDustSum)
}
// We'll lower the fee from 6000sats/kWU to 253sats/kWU for our test.
fee := chainfee.SatPerKWeight(253)
err = aliceChannel.UpdateFee(fee)
require.NoError(t, err)
err = bobChannel.ReceiveUpdateFee(fee)
require.NoError(t, err)
err = ForceStateTransition(aliceChannel, bobChannel)
require.NoError(t, err)
// Create an HTLC that Bob will send to Alice which is above Alice's
// dust limit and below Bob's dust limit. This takes into account dust
// trimming for non-zero-fee channels.
htlc1Amt := lnwire.MilliSatoshi(700_000)
htlc1, preimage1 := createHTLC(0, htlc1Amt)
_, err = bobChannel.AddHTLC(htlc1, nil)
require.NoError(t, err)
_, err = aliceChannel.ReceiveHTLC(htlc1)
require.NoError(t, err)
// Assert that GetDustSum from Alice's perspective does not consider
// the HTLC dust on her commitment, but does on Bob's commitment.
checkDust(aliceChannel, lnwire.MilliSatoshi(0), htlc1Amt)
// Assert that GetDustSum from Bob's perspective results in the same
// conditions above holding.
checkDust(bobChannel, htlc1Amt, lnwire.MilliSatoshi(0))
// Forcing a state transition to occur should not change the dust sum.
err = ForceStateTransition(bobChannel, aliceChannel)
require.NoError(t, err)
checkDust(aliceChannel, lnwire.MilliSatoshi(0), htlc1Amt)
checkDust(bobChannel, htlc1Amt, lnwire.MilliSatoshi(0))
// Settling the HTLC back from Alice to Bob should not change the dust
// sum because the HTLC is counted until it's removed from the update
// logs via compactLogs.
err = aliceChannel.SettleHTLC(preimage1, uint64(0), nil, nil, nil)
require.NoError(t, err)
err = bobChannel.ReceiveHTLCSettle(preimage1, uint64(0))
require.NoError(t, err)
checkDust(aliceChannel, lnwire.MilliSatoshi(0), htlc1Amt)
checkDust(bobChannel, htlc1Amt, lnwire.MilliSatoshi(0))
// Forcing a state transition will remove the HTLC in-memory for Bob
// since ReceiveRevocation is called which calls compactLogs. Bob
// should have a zero dust sum at this point. Alice will see Bob as
// having the original dust sum since compactLogs hasn't been called.
err = ForceStateTransition(aliceChannel, bobChannel)
require.NoError(t, err)
checkDust(aliceChannel, lnwire.MilliSatoshi(0), htlc1Amt)
checkDust(bobChannel, lnwire.MilliSatoshi(0), lnwire.MilliSatoshi(0))
// Alice now sends an HTLC of 100sats, which is below both sides' dust
// limits.
htlc2Amt := lnwire.MilliSatoshi(100_000)
htlc2, _ := createHTLC(0, htlc2Amt)
_, err = aliceChannel.AddHTLC(htlc2, nil)
require.NoError(t, err)
_, err = bobChannel.ReceiveHTLC(htlc2)
require.NoError(t, err)
// Assert that GetDustSum from Alice's perspective includes the new
// HTLC as dust on both commitments.
checkDust(aliceChannel, htlc2Amt, htlc1Amt+htlc2Amt)
// Assert that GetDustSum from Bob's perspective also includes the HTLC
// on both commitments.
checkDust(bobChannel, htlc2Amt, htlc2Amt)
// Alice signs for this HTLC and neither perspective should change.
aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
require.NoError(t, err)
err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
require.NoError(t, err)
checkDust(aliceChannel, htlc2Amt, htlc1Amt+htlc2Amt)
checkDust(bobChannel, htlc2Amt, htlc2Amt)
// Bob now sends a revocation for his prior commitment, and this should
// change Alice's perspective to no longer include the first HTLC as
// dust.
bobRevocation, _, err := bobChannel.RevokeCurrentCommitment()
require.NoError(t, err)
_, _, _, _, err = aliceChannel.ReceiveRevocation(bobRevocation)
require.NoError(t, err)
checkDust(aliceChannel, htlc2Amt, htlc2Amt)
checkDust(bobChannel, htlc2Amt, htlc2Amt)
// The rest of the dance is completed and neither perspective should
// change.
bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
require.NoError(t, err)
err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
require.NoError(t, err)
aliceRevocation, _, err := aliceChannel.RevokeCurrentCommitment()
require.NoError(t, err)
_, _, _, _, err = bobChannel.ReceiveRevocation(aliceRevocation)
require.NoError(t, err)
checkDust(aliceChannel, htlc2Amt, htlc2Amt)
checkDust(bobChannel, htlc2Amt, htlc2Amt)
// We'll now assert that if Alice sends an HTLC above her dust limit
// and then updates the fee of the channel to trigger the trimmed to
// dust mechanism, Alice will count this HTLC in the dust sum for her
// commitment in the non-zero-fee case.
htlc3Amt := lnwire.MilliSatoshi(400_000)
htlc3, _ := createHTLC(1, htlc3Amt)
_, err = aliceChannel.AddHTLC(htlc3, nil)
require.NoError(t, err)
_, err = bobChannel.ReceiveHTLC(htlc3)
require.NoError(t, err)
// Assert that this new HTLC is not counted on Alice's local commitment
// in the dust sum. Bob's commitment should count it.
checkDust(aliceChannel, htlc2Amt, htlc2Amt+htlc3Amt)
checkDust(bobChannel, htlc2Amt+htlc3Amt, htlc2Amt)
// Alice will now send UpdateFee with a large feerate and neither
// perspective should change.
fee = chainfee.SatPerKWeight(50_000)
err = aliceChannel.UpdateFee(fee)
require.NoError(t, err)
err = bobChannel.ReceiveUpdateFee(fee)
require.NoError(t, err)
checkDust(aliceChannel, htlc2Amt, htlc2Amt+htlc3Amt)
checkDust(bobChannel, htlc2Amt+htlc3Amt, htlc2Amt)
// Forcing a state transition should change in the non-zero-fee case.
err = ForceStateTransition(aliceChannel, bobChannel)
require.NoError(t, err)
if chantype.ZeroHtlcTxFee() {
checkDust(aliceChannel, htlc2Amt, htlc2Amt+htlc3Amt)
checkDust(bobChannel, htlc2Amt+htlc3Amt, htlc2Amt)
} else {
checkDust(aliceChannel, htlc2Amt+htlc3Amt, htlc2Amt+htlc3Amt)
checkDust(bobChannel, htlc2Amt+htlc3Amt, htlc2Amt+htlc3Amt)
}
}

View File

@ -440,7 +440,7 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance,
numHTLCs := int64(0)
for _, htlc := range filteredHTLCView.ourUpdates {
if htlcIsDust(
if HtlcIsDust(
cb.chanState.ChanType, false, isOurs, feePerKw,
htlc.Amount.ToSatoshis(), dustLimit,
) {
@ -450,7 +450,7 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance,
numHTLCs++
}
for _, htlc := range filteredHTLCView.theirUpdates {
if htlcIsDust(
if HtlcIsDust(
cb.chanState.ChanType, true, isOurs, feePerKw,
htlc.Amount.ToSatoshis(), dustLimit,
) {
@ -529,7 +529,7 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance,
// purposes of sorting.
cltvs := make([]uint32, len(commitTx.TxOut))
for _, htlc := range filteredHTLCView.ourUpdates {
if htlcIsDust(
if HtlcIsDust(
cb.chanState.ChanType, false, isOurs, feePerKw,
htlc.Amount.ToSatoshis(), dustLimit,
) {
@ -546,7 +546,7 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance,
cltvs = append(cltvs, htlc.Timeout)
}
for _, htlc := range filteredHTLCView.theirUpdates {
if htlcIsDust(
if HtlcIsDust(
cb.chanState.ChanType, true, isOurs, feePerKw,
htlc.Amount.ToSatoshis(), dustLimit,
) {

View File

@ -93,6 +93,9 @@ var (
0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, 0x08, 0x22,
0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09,
}
aliceDustLimit = btcutil.Amount(200)
bobDustLimit = btcutil.Amount(1300)
)
// CreateTestChannels creates to fully populated channels to be used within
@ -112,8 +115,6 @@ func CreateTestChannels(chanType channeldb.ChannelType) (
}
channelBal := channelCapacity / 2
aliceDustLimit := btcutil.Amount(200)
bobDustLimit := btcutil.Amount(1300)
csvTimeoutAlice := uint32(5)
csvTimeoutBob := uint32(4)

View File

@ -365,6 +365,11 @@
; propagation (default: 10)
; max-commit-fee-rate-anchors=5
; A threshold defining the maximum amount of dust a given channel can have
; after which forwarding and sending dust HTLC's to and from the channel will
; fail. This amount is expressed in satoshis. (default: 500000)
; dust-threshold=1000000
; If true, lnd will abort committing a migration if it would otherwise have been
; successful. This leaves the database unmodified, and still compatible with the
; previously active version of lnd.

View File

@ -490,6 +490,9 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
s.htlcNotifier = htlcswitch.NewHtlcNotifier(time.Now)
thresholdSats := btcutil.Amount(cfg.DustThreshold)
thresholdMSats := lnwire.NewMSatFromSatoshis(thresholdSats)
s.htlcSwitch, err = htlcswitch.New(htlcswitch.Config{
DB: dbs.chanStateDB,
LocalChannelClose: func(pubKey []byte,
@ -519,6 +522,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
RejectHTLC: cfg.RejectHTLC,
Clock: clock.NewDefaultClock(),
HTLCExpiry: htlcswitch.DefaultHTLCExpiry,
DustThreshold: thresholdMSats,
}, uint32(currentHeight))
if err != nil {
return nil, err