diff --git a/config.go b/config.go index d7e3aefd..7a13e58a 100644 --- a/config.go +++ b/config.go @@ -33,6 +33,7 @@ import ( "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lnrpc/signrpc" + "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/routing" "github.com/lightningnetwork/lnd/tor" ) @@ -289,6 +290,8 @@ type Config struct { MaxChannelFeeAllocation float64 `long:"max-channel-fee-allocation" description:"The maximum percentage of total funds that can be allocated to a channel's commitment fee. This only applies for the initiator of the channel. Valid values are within [0.1, 1]."` + MaxCommitFeeRateAnchors uint64 `long:"max-commit-fee-rate-anchors" description:"The maximum fee rate in sat/vbyte that will be used for commitments of channels of the anchors type. Must be large enough to ensure transaction propagation"` + DryRunMigration bool `long:"dry-run-migration" description:"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."` net tor.Net @@ -475,6 +478,7 @@ func DefaultConfig() Config { }, MaxOutgoingCltvExpiry: htlcswitch.DefaultMaxOutgoingCltvExpiry, MaxChannelFeeAllocation: htlcswitch.DefaultMaxLinkFeeAllocation, + MaxCommitFeeRateAnchors: lnwallet.DefaultAnchorsCommitMaxFeeRateSatPerVByte, LogWriter: build.NewRotatingLogWriter(), DB: lncfg.DefaultDB(), registeredChains: chainreg.NewChainRegistry(), @@ -735,6 +739,12 @@ func ValidateConfig(cfg Config, usageMessage string) (*Config, error) { cfg.MaxChannelFeeAllocation) } + if cfg.MaxCommitFeeRateAnchors < 1 { + return nil, fmt.Errorf("invalid max commit fee rate anchors: "+ + "%v, must be at least 1 sat/vbyte", + cfg.MaxCommitFeeRateAnchors) + } + // Validate the Tor config parameters. socks, err := lncfg.ParseAddressString( cfg.Tor.SOCKS, strconv.Itoa(defaultTorSOCKSPort), diff --git a/fundingmanager.go b/fundingmanager.go index 9d1e234c..219f4c58 100644 --- a/fundingmanager.go +++ b/fundingmanager.go @@ -366,6 +366,10 @@ type fundingConfig struct { // RegisteredChains keeps track of all chains that have been registered // with the daemon. RegisteredChains *chainreg.ChainRegistry + + // MaxAnchorsCommitFeeRate is the max commitment fee rate we'll use as + // the initiator for channels of the anchor type. + MaxAnchorsCommitFeeRate chainfee.SatPerKWeight } // fundingManager acts as an orchestrator/bridge between the wallet's @@ -3122,16 +3126,6 @@ func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) { msg.pushAmt, msg.chainHash, peerKey.SerializeCompressed(), ourDustLimit, msg.minConfs) - // First, we'll query the fee estimator for a fee that should get the - // commitment transaction confirmed by the next few blocks (conf target - // of 3). We target the near blocks here to ensure that we'll be able - // to execute a timely unilateral channel closure if needed. - commitFeePerKw, err := f.cfg.FeeEstimator.EstimateFeePerKW(3) - if err != nil { - msg.err <- err - return - } - // We set the channel flags to indicate whether we want this channel to // be announced to the network. var channelFlags lnwire.FundingFlag @@ -3190,6 +3184,25 @@ func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) { commitType := commitmentType( msg.peer.LocalFeatures(), msg.peer.RemoteFeatures(), ) + + // First, we'll query the fee estimator for a fee that should get the + // commitment transaction confirmed by the next few blocks (conf target + // of 3). We target the near blocks here to ensure that we'll be able + // to execute a timely unilateral channel closure if needed. + commitFeePerKw, err := f.cfg.FeeEstimator.EstimateFeePerKW(3) + if err != nil { + msg.err <- err + return + } + + // For anchor channels cap the initial commit fee rate at our defined + // maximum. + if commitType == lnwallet.CommitmentTypeAnchorsZeroFeeHtlcTx && + commitFeePerKw > f.cfg.MaxAnchorsCommitFeeRate { + + commitFeePerKw = f.cfg.MaxAnchorsCommitFeeRate + } + req := &lnwallet.InitFundingReserveMsg{ ChainHash: &msg.chainHash, PendingChanID: chanID, diff --git a/htlcswitch/link.go b/htlcswitch/link.go index df6bafc9..9d6ad673 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -269,6 +269,10 @@ type ChannelLinkConfig struct { // initiator of the channel. MaxFeeAllocation float64 + // MaxAnchorsCommitFeeRate is the max commitment fee rate we'll use as + // the initiator for channels of the anchor type. + MaxAnchorsCommitFeeRate chainfee.SatPerKWeight + // NotifyActiveLink allows the link to tell the ChannelNotifier when a // link is first started. NotifyActiveLink func(wire.OutPoint) @@ -1090,7 +1094,10 @@ func (l *channelLink) htlcManager() { // based on our current set fee rate. We'll cap the new // fee rate to our max fee allocation. commitFee := l.channel.CommitFeeRate() - maxFee := l.channel.MaxFeeRate(l.cfg.MaxFeeAllocation) + maxFee := l.channel.MaxFeeRate( + l.cfg.MaxFeeAllocation, + l.cfg.MaxAnchorsCommitFeeRate, + ) newCommitFee := chainfee.SatPerKWeight( math.Min(float64(netFee), float64(maxFee)), ) diff --git a/htlcswitch/test_utils.go b/htlcswitch/test_utils.go index 6fc6b1dd..7d14abce 100644 --- a/htlcswitch/test_utils.go +++ b/htlcswitch/test_utils.go @@ -1185,6 +1185,7 @@ func (h *hopNetwork) createChannelLink(server, peer *mockServer, OutgoingCltvRejectDelta: 3, MaxOutgoingCltvExpiry: DefaultMaxOutgoingCltvExpiry, MaxFeeAllocation: DefaultMaxLinkFeeAllocation, + MaxAnchorsCommitFeeRate: chainfee.SatPerKVByte(10 * 1000).FeePerKWeight(), NotifyActiveLink: func(wire.OutPoint) {}, NotifyActiveChannel: func(wire.OutPoint) {}, NotifyInactiveChannel: func(wire.OutPoint) {}, diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 90f30e58..6cd4389a 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -6809,11 +6809,14 @@ func (lc *LightningChannel) CalcFee(feeRate chainfee.SatPerKWeight) btcutil.Amou // MaxFeeRate returns the maximum fee rate given an allocation of the channel // initiator's spendable balance. This can be useful to determine when we should -// stop proposing fee updates that exceed our maximum allocation. +// stop proposing fee updates that exceed our maximum allocation. We also take +// a fee rate cap that should be used for anchor type channels. // // NOTE: This should only be used for channels in which the local commitment is // the initiator. -func (lc *LightningChannel) MaxFeeRate(maxAllocation float64) chainfee.SatPerKWeight { +func (lc *LightningChannel) MaxFeeRate(maxAllocation float64, + maxAnchorFeeRate chainfee.SatPerKWeight) chainfee.SatPerKWeight { + lc.RLock() defer lc.RUnlock() @@ -6828,9 +6831,16 @@ func (lc *LightningChannel) MaxFeeRate(maxAllocation float64) chainfee.SatPerKWe // Ensure the fee rate doesn't dip below the fee floor. _, weight := lc.availableBalance() maxFeeRate := maxFee / (float64(weight) / 1000) - return chainfee.SatPerKWeight( + feeRate := chainfee.SatPerKWeight( math.Max(maxFeeRate, float64(chainfee.FeePerKwFloor)), ) + + // Cap anchor fee rates. + if lc.channelState.ChanType.HasAnchors() && feeRate > maxAnchorFeeRate { + return maxAnchorFeeRate + } + + return feeRate } // RemoteNextRevocation returns the channelState's RemoteNextRevocation. diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index b69b2faf..ead91598 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -7996,6 +7996,19 @@ func TestForceCloseBorkedState(t *testing.T) { func TestChannelMaxFeeRate(t *testing.T) { t.Parallel() + assertMaxFeeRate := func(c *LightningChannel, + maxAlloc float64, anchorMax, expFeeRate chainfee.SatPerKWeight) { + + t.Helper() + + maxFeeRate := c.MaxFeeRate(maxAlloc, anchorMax) + if maxFeeRate != expFeeRate { + t.Fatalf("expected max fee rate of %v with max "+ + "allocation of %v, got %v", expFeeRate, + maxAlloc, maxFeeRate) + } + } + aliceChannel, _, cleanUp, err := CreateTestChannels( channeldb.SingleFunderTweaklessBit, ) @@ -8004,21 +8017,32 @@ func TestChannelMaxFeeRate(t *testing.T) { } defer cleanUp() - assertMaxFeeRate := func(maxAlloc float64, - expFeeRate chainfee.SatPerKWeight) { + assertMaxFeeRate(aliceChannel, 1.0, 0, 690607734) + assertMaxFeeRate(aliceChannel, 0.001, 0, 690607) + assertMaxFeeRate(aliceChannel, 0.000001, 0, 690) + assertMaxFeeRate(aliceChannel, 0.0000001, 0, chainfee.FeePerKwFloor) - maxFeeRate := aliceChannel.MaxFeeRate(maxAlloc) - if maxFeeRate != expFeeRate { - t.Fatalf("expected max fee rate of %v with max "+ - "allocation of %v, got %v", expFeeRate, - maxAlloc, maxFeeRate) - } + // Check that anchor channels are capped at their max fee rate. + anchorChannel, _, cleanUp, err := CreateTestChannels( + channeldb.SingleFunderTweaklessBit | channeldb.AnchorOutputsBit, + ) + if err != nil { + t.Fatalf("unable to create test channels: %v", err) } + defer cleanUp() - assertMaxFeeRate(1.0, 690607734) - assertMaxFeeRate(0.001, 690607) - assertMaxFeeRate(0.000001, 690) - assertMaxFeeRate(0.0000001, chainfee.FeePerKwFloor) + // Anchor commitments are heavier, hence will the same allocation lead + // to slightly lower fee rates. + assertMaxFeeRate( + anchorChannel, 1.0, chainfee.FeePerKwFloor, + chainfee.FeePerKwFloor, + ) + assertMaxFeeRate(anchorChannel, 0.001, 1000000, 444839) + assertMaxFeeRate(anchorChannel, 0.001, 300000, 300000) + assertMaxFeeRate(anchorChannel, 0.000001, 700, 444) + assertMaxFeeRate( + anchorChannel, 0.0000001, 1000000, chainfee.FeePerKwFloor, + ) } // TestChannelFeeRateFloor asserts that valid commitments can be proposed and diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index 9dbf19b1..4d58822d 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -17,6 +17,11 @@ import ( // anchorSize is the constant anchor output size. const anchorSize = btcutil.Amount(330) +// DefaultAnchorsCommitMaxFeeRateSatPerVByte is the default max fee rate in +// sat/vbyte the initiator will use for anchor channels. This should be enough +// to ensure propagation before anchoring down the commitment transaction. +const DefaultAnchorsCommitMaxFeeRateSatPerVByte = 10 + // CommitmentKeyRing holds all derived keys needed to construct commitment and // HTLC transactions. The keys are derived differently depending whether the // commitment transaction is ours or the remote peer's. Private keys associated diff --git a/peer/brontide.go b/peer/brontide.go index d3089313..49c3d8e4 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -294,6 +294,10 @@ type Config struct { // commitment fee. This only applies for the initiator of the channel. MaxChannelFeeAllocation float64 + // MaxAnchorsCommitFeeRate is the maximum fee rate we'll use as an + // initiator for anchor channel commitments. + MaxAnchorsCommitFeeRate chainfee.SatPerKWeight + // ServerPubKey is the serialized, compressed public key of our lnd node. // It is used to determine which policy (channel edge) to pass to the // ChannelLink. @@ -815,6 +819,7 @@ func (p *Brontide) addLink(chanPoint *wire.OutPoint, TowerClient: towerClient, MaxOutgoingCltvExpiry: p.cfg.MaxOutgoingCltvExpiry, MaxFeeAllocation: p.cfg.MaxChannelFeeAllocation, + MaxAnchorsCommitFeeRate: p.cfg.MaxAnchorsCommitFeeRate, NotifyActiveLink: p.cfg.ChannelNotifier.NotifyActiveLinkEvent, NotifyActiveChannel: p.cfg.ChannelNotifier.NotifyActiveChannelEvent, NotifyInactiveChannel: p.cfg.ChannelNotifier.NotifyInactiveChannelEvent, diff --git a/server.go b/server.go index 5291b566..d59c25a1 100644 --- a/server.go +++ b/server.go @@ -1185,6 +1185,8 @@ func newServer(cfg *Config, listenAddrs []net.Addr, NotifyPendingOpenChannelEvent: s.channelNotifier.NotifyPendingOpenChannelEvent, EnableUpfrontShutdown: cfg.EnableUpfrontShutdown, RegisteredChains: cfg.registeredChains, + MaxAnchorsCommitFeeRate: chainfee.SatPerKVByte( + s.cfg.MaxCommitFeeRateAnchors * 1000).FeePerKWeight(), }) if err != nil { return nil, err @@ -3119,7 +3121,9 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq, UnsafeReplay: s.cfg.UnsafeReplay, MaxOutgoingCltvExpiry: s.cfg.MaxOutgoingCltvExpiry, MaxChannelFeeAllocation: s.cfg.MaxChannelFeeAllocation, - Quit: s.quit, + MaxAnchorsCommitFeeRate: chainfee.SatPerKVByte( + s.cfg.MaxCommitFeeRateAnchors * 1000).FeePerKWeight(), + Quit: s.quit, } copy(pCfg.PubKeyBytes[:], peerAddr.IdentityKey.SerializeCompressed())