diff --git a/config.go b/config.go index 4125c21d..3a0af8df 100644 --- a/config.go +++ b/config.go @@ -317,6 +317,8 @@ type config struct { MaxOutgoingCltvExpiry uint32 `long:"max-cltv-expiry" description:"The maximum number of blocks funds could be locked up for when forwarding payments."` + 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]."` + net tor.Net Routing *routing.Conf `group:"routing" namespace:"routing"` @@ -432,7 +434,8 @@ func loadConfig() (*config, error) { Watchtower: &lncfg.Watchtower{ TowerDir: defaultTowerDir, }, - MaxOutgoingCltvExpiry: htlcswitch.DefaultMaxOutgoingCltvExpiry, + MaxOutgoingCltvExpiry: htlcswitch.DefaultMaxOutgoingCltvExpiry, + MaxChannelFeeAllocation: htlcswitch.DefaultMaxLinkFeeAllocation, } // Pre-parse the command line options to pick up an alternative config @@ -591,6 +594,13 @@ func loadConfig() (*config, error) { return nil, err } + // Ensure a valid max channel fee allocation was set. + if cfg.MaxChannelFeeAllocation <= 0 || cfg.MaxChannelFeeAllocation > 1 { + return nil, fmt.Errorf("invalid max channel fee allocation: "+ + "%v, must be within (0, 1]", + cfg.MaxChannelFeeAllocation) + } + // Validate the Tor config parameters. socks, err := lncfg.ParseAddressString( cfg.Tor.SOCKS, strconv.Itoa(defaultTorSOCKSPort), diff --git a/htlcswitch/link.go b/htlcswitch/link.go index 7d14b7af..c054ea0d 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/sha256" "fmt" + "math" prand "math/rand" "sync" "sync/atomic" @@ -50,6 +51,11 @@ const ( // DefaultMaxLinkFeeUpdateTimeout represents the maximum interval in // which a link should propose to update its commitment fee rate. DefaultMaxLinkFeeUpdateTimeout = 60 * time.Minute + + // DefaultMaxLinkFeeAllocation is the highest allocation we'll allow + // a channel's commitment fee to be of its balance. This only applies to + // the initiator of the channel. + DefaultMaxLinkFeeAllocation float64 = 0.5 ) // ForwardingPolicy describes the set of constraints that a given ChannelLink @@ -250,6 +256,11 @@ type ChannelLinkConfig struct { // accept for a forwarded HTLC. The value is relative to the current // block height. MaxOutgoingCltvExpiry uint32 + + // MaxFeeAllocation is the highest allocation we'll allow a channel's + // commitment fee to be of its balance. This only applies to the + // initiator of the channel. + MaxFeeAllocation float64 } // channelLink is the service which drives a channel's commitment update @@ -995,22 +1006,27 @@ out: // If we are the initiator, then we'll sample the // current fee rate to get into the chain within 3 // blocks. - feePerKw, err := l.sampleNetworkFee() + netFee, err := l.sampleNetworkFee() if err != nil { log.Errorf("unable to sample network fee: %v", err) continue } // We'll check to see if we should update the fee rate - // based on our current set fee rate. + // based on our current set fee rate. We'll cap the new + // fee rate to our max fee allocation. commitFee := l.channel.CommitFeeRate() - if !shouldAdjustCommitFee(feePerKw, commitFee) { + maxFee := l.channel.MaxFeeRate(l.cfg.MaxFeeAllocation) + newCommitFee := lnwallet.SatPerKWeight( + math.Min(float64(netFee), float64(maxFee)), + ) + if !shouldAdjustCommitFee(newCommitFee, commitFee) { continue } // If we do, then we'll send a new UpdateFee message to // the remote party, to be locked in with a new update. - if err := l.updateChannelFee(feePerKw); err != nil { + if err := l.updateChannelFee(newCommitFee); err != nil { log.Errorf("unable to update fee rate: %v", err) continue } diff --git a/htlcswitch/link_test.go b/htlcswitch/link_test.go index e68f0c90..178778f5 100644 --- a/htlcswitch/link_test.go +++ b/htlcswitch/link_test.go @@ -1686,6 +1686,7 @@ func newSingleLinkTestHarness(chanAmt, chanReserve btcutil.Amount) ( MinFeeUpdateTimeout: 30 * time.Minute, MaxFeeUpdateTimeout: 40 * time.Minute, MaxOutgoingCltvExpiry: DefaultMaxOutgoingCltvExpiry, + MaxFeeAllocation: DefaultMaxLinkFeeAllocation, } const startingHeight = 100 @@ -3736,9 +3737,10 @@ func TestChannelLinkUpdateCommitFee(t *testing.T) { // First, we'll create our traditional three hop network. We'll only be // interacting with and asserting the state of two of the end points // for this test. + const aliceInitialBalance = btcutil.SatoshiPerBitcoin * 3 channels, cleanUp, _, err := createClusterChannels( - btcutil.SatoshiPerBitcoin*3, - btcutil.SatoshiPerBitcoin*5) + aliceInitialBalance, btcutil.SatoshiPerBitcoin*5, + ) if err != nil { t.Fatalf("unable to create channel: %v", err) } @@ -3757,8 +3759,15 @@ func TestChannelLinkUpdateCommitFee(t *testing.T) { {"alice", "bob", &lnwire.FundingLocked{}, false}, {"bob", "alice", &lnwire.FundingLocked{}, false}, + // First fee update. {"alice", "bob", &lnwire.UpdateFee{}, false}, + {"alice", "bob", &lnwire.CommitSig{}, false}, + {"bob", "alice", &lnwire.RevokeAndAck{}, false}, + {"bob", "alice", &lnwire.CommitSig{}, false}, + {"alice", "bob", &lnwire.RevokeAndAck{}, false}, + // Second fee update. + {"alice", "bob", &lnwire.UpdateFee{}, false}, {"alice", "bob", &lnwire.CommitSig{}, false}, {"bob", "alice", &lnwire.RevokeAndAck{}, false}, {"bob", "alice", &lnwire.CommitSig{}, false}, @@ -3779,7 +3788,7 @@ func TestChannelLinkUpdateCommitFee(t *testing.T) { // triggerFeeUpdate is a helper closure to determine whether a fee // update was triggered and completed properly. - triggerFeeUpdate := func(newFeeRate lnwallet.SatPerKWeight, + triggerFeeUpdate := func(feeEstimate, newFeeRate lnwallet.SatPerKWeight, shouldUpdate bool) { t.Helper() @@ -3795,7 +3804,7 @@ func TestChannelLinkUpdateCommitFee(t *testing.T) { // Next, we'll send the first fee rate response to Alice. select { - case n.feeEstimator.byteFeeIn <- newFeeRate: + case n.feeEstimator.byteFeeIn <- feeEstimate: case <-time.After(time.Second * 5): t.Fatalf("alice didn't query for the new network fee") } @@ -3830,11 +3839,18 @@ func TestChannelLinkUpdateCommitFee(t *testing.T) { // Triggering the link to update the fee of the channel with the same // fee rate should not send a fee update. - triggerFeeUpdate(startingFeeRate, false) + triggerFeeUpdate(startingFeeRate, startingFeeRate, false) // Triggering the link to update the fee of the channel with a much // larger fee rate _should_ send a fee update. - triggerFeeUpdate(startingFeeRate*3, true) + newFeeRate := startingFeeRate * 3 + triggerFeeUpdate(newFeeRate, newFeeRate, true) + + // Triggering the link to update the fee of the channel with a fee rate + // that exceeds its maximum fee allocation should result in a fee rate + // corresponding to the maximum fee allocation. + const maxFeeRate lnwallet.SatPerKWeight = 207182320 + triggerFeeUpdate(maxFeeRate+1, maxFeeRate, true) } // TestChannelLinkAcceptDuplicatePayment tests that if a link receives an @@ -4236,6 +4252,7 @@ func (h *persistentLinkHarness) restartLink( // Set any hodl flags requested for the new link. HodlMask: hodl.MaskFromFlags(hodlFlags...), MaxOutgoingCltvExpiry: DefaultMaxOutgoingCltvExpiry, + MaxFeeAllocation: DefaultMaxLinkFeeAllocation, } const startingHeight = 100 diff --git a/htlcswitch/test_utils.go b/htlcswitch/test_utils.go index 212e20e3..b3945b21 100644 --- a/htlcswitch/test_utils.go +++ b/htlcswitch/test_utils.go @@ -1116,6 +1116,7 @@ func (h *hopNetwork) createChannelLink(server, peer *mockServer, OnChannelFailure: func(lnwire.ChannelID, lnwire.ShortChannelID, LinkFailureError) {}, OutgoingCltvRejectDelta: 3, MaxOutgoingCltvExpiry: DefaultMaxOutgoingCltvExpiry, + MaxFeeAllocation: DefaultMaxLinkFeeAllocation, }, channel, ) diff --git a/peer.go b/peer.go index cad00dcb..c57f23a4 100644 --- a/peer.go +++ b/peer.go @@ -585,6 +585,7 @@ func (p *peer) addLink(chanPoint *wire.OutPoint, OutgoingCltvRejectDelta: p.outgoingCltvRejectDelta, TowerClient: p.server.towerClient, MaxOutgoingCltvExpiry: cfg.MaxOutgoingCltvExpiry, + MaxFeeAllocation: cfg.MaxChannelFeeAllocation, } link := htlcswitch.NewChannelLink(linkCfg, lnChan)