diff --git a/lnwallet/channel.go b/lnwallet/channel.go index aa3f38650..17e84acec 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -7128,6 +7128,7 @@ func newOutgoingHtlcResolution(signer input.Signer, localDelayTweak := input.SingleTweakBytes( keyRing.CommitPoint, localChanCfg.DelayBasePoint.PubKey, ) + return &OutgoingHtlcResolution{ Expiry: htlc.RefundTimeout, SignedTimeoutTx: timeoutTx, @@ -8663,10 +8664,17 @@ func (lc *LightningChannel) CalcFee(feeRate chainfee.SatPerKWeight) btcutil.Amou // initiator's spendable balance along with the local reserve amount. This can // be useful to determine when we should stop proposing fee updates that exceed // our maximum allocation. +// Moreover it returns the share of the total balance in the range of [0,1] +// which can be allocated to fees. When our desired fee allocation would lead to +// a maximum fee rate below the current commitment fee rate we floor the maximum +// at the current fee rate which leads to different fee allocations than +// initially requested via `maxAllocation`. // // 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) (chainfee.SatPerKWeight, float64) { + lc.RLock() defer lc.RUnlock() @@ -8676,18 +8684,25 @@ func (lc *LightningChannel) MaxFeeRate(maxAllocation float64) chainfee.SatPerKWe // exactly why it was introduced to react for sharp fee changes. availableBalance, weight := lc.availableBalance(AdditionalHtlc) - oldFee := lc.localCommitChain.tip().feePerKw.FeeForWeight(weight) + currentFee := lc.localCommitChain.tip().feePerKw.FeeForWeight(weight) // baseBalance is the maximum amount available for us to spend on fees. - baseBalance := availableBalance.ToSatoshis() + oldFee + baseBalance := availableBalance.ToSatoshis() + currentFee - maxFee := float64(baseBalance) * maxAllocation + // In case our local channel balance is drained, we make sure we do not + // decrease the fee rate below the current fee rate. This could lead to + // a scenario where we lower the commitment fee rate as low as the fee + // floor although current fee rates are way higher. The maximum fee + // we allow should not be smaller then the current fee. The decrease + // in fee rate should happen when the mempool reports lower fee levels + // rather than us decreasing in local balance. The max fee rate is + // always floored by the current fee rate of the channel. + idealMaxFee := float64(baseBalance) * maxAllocation + maxFee := math.Max(float64(currentFee), idealMaxFee) + maxFeeAllocation := maxFee / float64(baseBalance) + maxFeeRate := chainfee.SatPerKWeight(maxFee / (float64(weight) / 1000)) - // Ensure the fee rate doesn't dip below the fee floor. - maxFeeRate := maxFee / (float64(weight) / 1000) - return chainfee.SatPerKWeight( - math.Max(maxFeeRate, float64(chainfee.FeePerKwFloor)), - ) + return maxFeeRate, maxFeeAllocation } // IdealCommitFeeRate uses the current network fee, the minimum relay fee, @@ -8699,7 +8714,7 @@ func (lc *LightningChannel) IdealCommitFeeRate(netFeeRate, minRelayFeeRate, // Get the maximum fee rate that we can use given our max fee allocation // and given the local reserve balance that we must preserve. - maxFeeRate := lc.MaxFeeRate(maxFeeAlloc) + maxFeeRate, _ := lc.MaxFeeRate(maxFeeAlloc) var commitFeeRate chainfee.SatPerKWeight @@ -8738,7 +8753,7 @@ func (lc *LightningChannel) IdealCommitFeeRate(netFeeRate, minRelayFeeRate, // use. If it is, then we use the minimum relay fee rate and we log a // warning to indicate that the max channel fee allocation option was // ignored. - absoluteMaxFee := lc.MaxFeeRate(1) + absoluteMaxFee, _ := lc.MaxFeeRate(1) if minRelayFeeRate <= absoluteMaxFee { lc.log.Warn("Ignoring max channel fee allocation to " + "ensure that the commitment fee is above the " + diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index aa747bc42..53c93aab2 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -7515,67 +7515,146 @@ func TestForceCloseBorkedState(t *testing.T) { } // TestChannelMaxFeeRate ensures we correctly compute a channel initiator's max -// fee rate based on an allocation and its available balance. It should never -// dip below the established fee floor. +// fee rate based on an allocation and its available balance. When a very low +// fee allocation value is selected the max fee rate is always floored at the +// current fee rate of the channel. func TestChannelMaxFeeRate(t *testing.T) { t.Parallel() - assertMaxFeeRate := func(c *LightningChannel, maxAlloc float64, - expFeeRate chainfee.SatPerKWeight) { - - maxFeeRate := c.MaxFeeRate(maxAlloc) - if maxFeeRate != expFeeRate { - t.Fatalf("expected max fee rate of %v with max "+ - "allocation of %v, got %v", expFeeRate, - maxAlloc, maxFeeRate) - } - - if err := c.validateFeeRate(maxFeeRate); err != nil { - t.Fatalf("fee rate validation failed: %v", err) - } - } - // propertyTest tests that the validateFeeRate function always passes // for the output returned by MaxFeeRate for any valid random inputs // fed to MaxFeeRate. propertyTest := func(c *LightningChannel) func(alloc maxAlloc) bool { return func(ma maxAlloc) bool { - maxFeeRate := c.MaxFeeRate(float64(ma)) + maxFeeRate, _ := c.MaxFeeRate(float64(ma)) return c.validateFeeRate(maxFeeRate) == nil } } - aliceChannel, _, err := CreateTestChannels( + // Create a non-anchor and an anchor channel setup. + nonAnchorChannel, _, err := CreateTestChannels( t, channeldb.SingleFunderTweaklessBit, ) require.NoError(t, err, "unable to create test channels") - if err := quick.Check(propertyTest(aliceChannel), nil); err != nil { - t.Fatal(err) - } - - assertMaxFeeRate(aliceChannel, 1.0, 676794154) - assertMaxFeeRate(aliceChannel, 0.001, 676794) - assertMaxFeeRate(aliceChannel, 0.000001, 676) - assertMaxFeeRate(aliceChannel, 0.0000001, chainfee.FeePerKwFloor) - - // Check that anchor channels are capped at their max fee rate. anchorChannel, _, err := CreateTestChannels( t, channeldb.SingleFunderTweaklessBit| channeldb.AnchorOutputsBit|channeldb.ZeroHtlcTxFeeBit, ) require.NoError(t, err, "unable to create test channels") - if err = quick.Check(propertyTest(anchorChannel), nil); err != nil { - t.Fatal(err) + // Run the property tests for both channel types. + err = quick.Check(propertyTest(nonAnchorChannel), nil) + require.NoError(t, err) + + err = quick.Check(propertyTest(anchorChannel), nil) + require.NoError(t, err) + + testCases := []struct { + name string + channel *LightningChannel + maxFeeAlloc float64 + expectedFeeAlloc float64 + expectedMinFeeRate bool + }{ + { + name: "non-anchor-channel high fee alloc", + channel: nonAnchorChannel, + maxFeeAlloc: 1.0, + expectedFeeAlloc: 1.0, + }, + { + name: "non-anchor-channel chan low fee " + + "alloc", + channel: nonAnchorChannel, + maxFeeAlloc: 1e-3, + expectedFeeAlloc: 1e-3, + }, + { + name: "anchor-channel high chan fee alloc", + channel: anchorChannel, + maxFeeAlloc: 1.0, + expectedFeeAlloc: 1.0, + }, + { + name: "anchor-channel low fee alloc", + channel: anchorChannel, + maxFeeAlloc: 1e-3, + expectedFeeAlloc: 1e-3, + }, + { + // The fee rate is capped at the current fee rate if the + // fee allocation is too low. + name: "non-anchor-channel current fee rate " + + "cap", + channel: nonAnchorChannel, + maxFeeAlloc: 1e-6, + expectedMinFeeRate: true, + }, + { + name: "non-anchor-channel current fee rate " + + "cap", + channel: nonAnchorChannel, + maxFeeAlloc: 1e-8, + expectedMinFeeRate: true, + }, + { + name: "anchor-channel current fee rate " + + "cap", + channel: anchorChannel, + maxFeeAlloc: 1e-6, + expectedMinFeeRate: true, + }, + { + name: "anchor-channel current fee rate " + + "cap", + channel: anchorChannel, + maxFeeAlloc: 1e-8, + expectedMinFeeRate: true, + }, } - // Anchor commitments are heavier, hence will the same allocation lead - // to slightly lower fee rates. - assertMaxFeeRate(anchorChannel, 1.0, 435941555) - assertMaxFeeRate(anchorChannel, 0.001, 435941) - assertMaxFeeRate(anchorChannel, 0.000001, 435) - assertMaxFeeRate(anchorChannel, 0.0000001, chainfee.FeePerKwFloor) + for _, testCase := range testCases { + tc := testCase + + maxFeeRate, feeAllocation := tc.channel.MaxFeeRate( + tc.maxFeeAlloc, + ) + + currentFeeRate := chainfee.SatPerKWeight( + tc.channel.channelState.LocalCommitment.FeePerKw, + ) + + // When the fee allocation would push our max fee rate below our + // current commitment fee rate of the channel we cap the max fee + // rate at the current fee rate of the channel. There might be + // some rounding inaccuracies due to the fee rate calculation + // therefore we accept a relative error of 0.1%. + if tc.expectedMinFeeRate { + require.InEpsilonf( + t, float64(currentFeeRate), float64(maxFeeRate), + 1e-3, "expected max fee rate:%d, got max "+ + "fee rate :%d", currentFeeRate, + maxFeeRate, + ) + } else { + // When the max fee rate is not capped because there + // is enough balance to allocate funds from we compare + // the fee allocation rather then the max fee rate so + // that we can reason more easily about the test values. + // Because of floating point operations we accept + // a relative error of 0.1%. + require.InEpsilonf( + t, tc.expectedFeeAlloc, feeAllocation, 1e-3, + "expected fee allocation:%f, got fee "+ + "allocation:%f", tc.expectedFeeAlloc, + feeAllocation, + ) + } + + err := tc.channel.validateFeeRate(maxFeeRate) + require.NoErrorf(t, err, "fee rate validation failed") + } } // TestIdealCommitFeeRate tests that we correctly compute the ideal commitment @@ -7584,23 +7663,6 @@ func TestChannelMaxFeeRate(t *testing.T) { func TestIdealCommitFeeRate(t *testing.T) { t.Parallel() - assertIdealFeeRate := func(c *LightningChannel, netFee, minRelay, - maxAnchorCommit chainfee.SatPerKWeight, - maxFeeAlloc float64, expectedFeeRate chainfee.SatPerKWeight) { - - feeRate := c.IdealCommitFeeRate( - netFee, minRelay, maxAnchorCommit, maxFeeAlloc, - ) - if feeRate != expectedFeeRate { - t.Fatalf("expected fee rate of %v got %v", - expectedFeeRate, feeRate) - } - - if err := c.validateFeeRate(feeRate); err != nil { - t.Fatalf("fee rate validation failed: %v", err) - } - } - // propertyTest tests that the validateFeeRate function always passes // for the output returned by IdealCommitFeeRate for any valid random // inputs fed to IdealCommitFeeRate. @@ -7621,115 +7683,251 @@ func TestIdealCommitFeeRate(t *testing.T) { } } - // Test ideal fee rates for a non-anchor channel + // Create a non-anchor channel and an anchor test channel. + nonAnchorChannel, _, err := CreateTestChannels( + t, channeldb.SingleFunderTweaklessBit, + ) + if err != nil { + t.Fatalf("unable to create test channels: %v", err) + } + + anchorChannel, _, err := CreateTestChannels( + t, channeldb.SingleFunderTweaklessBit| + channeldb.AnchorOutputsBit| + channeldb.ZeroHtlcTxFeeBit, + ) + if err != nil { + t.Fatalf("unable to create test channels: %v", err) + } + + // Run the property tests for both channel types (non-anchor and + // anchor). + err = quick.Check(propertyTest(nonAnchorChannel), nil) + if err != nil { + t.Fatal(err) + } + + err = quick.Check(propertyTest(anchorChannel), nil) + if err != nil { + t.Fatal(err) + } + + // maxFeeRate is a helper function which calculates the maximum fee rate + // a channel is allowed to allocate to fees. It does not take a minimum + // fee rate into account. + maxFeeRate := func(c *LightningChannel, + maxFeeAlloc float64) chainfee.SatPerKWeight { + + balance, weight := c.availableBalance(AdditionalHtlc) + feeRate := c.localCommitChain.tip().feePerKw + currentFee := feeRate.FeeForWeight(weight) + + maxBalance := balance.ToSatoshis() + currentFee + + maxFee := float64(maxBalance) * maxFeeAlloc + + maxFeeRate := chainfee.SatPerKWeight( + maxFee / (float64(weight) / 1000), + ) + + return maxFeeRate + } + + // currentFeeRate calculates the current fee rate of the channel. The + // ideal fee rate is floored at the current fee rate of the channel. + currentFeeRate := func(c *LightningChannel) chainfee.SatPerKWeight { + return c.localCommitChain.tip().feePerKw + } + + // testCase definies the test cases when calculating the ideal fee rate + // for both channel types (non-anchor and anchor channel). + type testCase struct { + name string + channel *LightningChannel + maxFeeAlloc float64 + networkFeeRate chainfee.SatPerKWeight + minRelayFee chainfee.SatPerKWeight + maxAnchorCommitFee chainfee.SatPerKWeight + expectedFeeRate chainfee.SatPerKWeight + } + + nonAnchorCases := []testCase{ + { + name: "ideal fee rate capped at the max " + + "available fee rate which is smaller than " + + "the network fee rate", + channel: nonAnchorChannel, + networkFeeRate: 7e8, + maxFeeAlloc: 1.0, + // There is not enough balance to pay the network fee so + // the maximum available fee rate is expected. + expectedFeeRate: maxFeeRate(nonAnchorChannel, 1.0), + }, + { + name: "enough balance for the ideal network " + + "fee rate", + channel: nonAnchorChannel, + networkFeeRate: 5e8, + maxFeeAlloc: 1.0, + expectedFeeRate: 5e8, + }, + { + name: "min relay fee rate is greater than " + + "the ideal network fee rate", + channel: nonAnchorChannel, + networkFeeRate: 5e8, + maxFeeAlloc: 1.0, + minRelayFee: 6e8, + // The min relay fee rate is used as ideal fee rate + // because we can afford it and it's greater than the + // network fee rate. + expectedFeeRate: 6e8, + }, + { + name: "max available fee rate is smaller than the " + + "min relay fee rate", + channel: nonAnchorChannel, + networkFeeRate: 8e8, + maxFeeAlloc: 1e-3, + minRelayFee: 7e8, + // Using 100% of the available balance (fee alloc) for + // the fee rate but this is still not enough to pay for + // the min relay fee rate. + expectedFeeRate: maxFeeRate(nonAnchorChannel, 1.0), + }, + { + name: "current fee rate of the channel is used as a" + + "fee floor because fee alloc is too small " + + "(fee ratcheting)", + channel: nonAnchorChannel, + networkFeeRate: 8e8, + maxFeeAlloc: 1e-7, + // The fee allocation is too small so we floor the ideal + // fee rate at the current fee rate of the channel. + expectedFeeRate: currentFeeRate(anchorChannel), + }, + } + + anchorCases := []testCase{ + { + name: "fee rate is capped at the max anchor commit " + + "fee rate, which is equal to the fee floor", + channel: anchorChannel, + networkFeeRate: 7e8, + maxFeeAlloc: 0.1, + maxAnchorCommitFee: chainfee.FeePerKwFloor, + expectedFeeRate: chainfee.FeePerKwFloor, + }, + { + name: "fee rate capped at the max anchor commit fee " + + "rate", + channel: anchorChannel, + networkFeeRate: 7e8, + maxFeeAlloc: 1e-6, + maxAnchorCommitFee: 700, + expectedFeeRate: 700, + }, + { + name: "fee rate is capped at the max commit anchor" + + "fee rate", + channel: anchorChannel, + networkFeeRate: 7e8, + maxFeeAlloc: 1e-3, + maxAnchorCommitFee: 1e5, + expectedFeeRate: 1e5, + }, + { + name: "min relay fee rate is used when " + + "the max commit anchor fee rate is smaller", + channel: anchorChannel, + networkFeeRate: 7e8, + maxFeeAlloc: 1e-4, + minRelayFee: 4e5, + maxAnchorCommitFee: 3e5, + expectedFeeRate: 4e5, + }, + { + name: "min relay fee rate is used when " + + "we can still can pay for it using 100% of " + + "our balance neglecting the initial fee alloc", + channel: anchorChannel, + networkFeeRate: 7e8, + maxFeeAlloc: 1e-3, + minRelayFee: 5e5, + maxAnchorCommitFee: 3e5, + expectedFeeRate: 5e5, + }, + { + name: "max fee rate using 100% of our balance, " + + "neglecting the initial fee alloc, but still " + + "not able to account for the min relay fee " + + "rate", + channel: anchorChannel, + networkFeeRate: 7e8, + maxFeeAlloc: 1e-3, + minRelayFee: 4.5e8, + maxAnchorCommitFee: 3e5, + // This is the absolute maximum balance which can be + // used for the commitment fee. + expectedFeeRate: maxFeeRate(anchorChannel, 1.0), + }, + { + name: "current fee rate of the " + + "commitment channel is used as the max " + + "commit anchor fee rate because the fee " + + "alloc. is too small (fee ratcheting)", + channel: anchorChannel, + networkFeeRate: 7e8, + maxFeeAlloc: 1e-7, + maxAnchorCommitFee: 1e6, + expectedFeeRate: currentFeeRate(anchorChannel), + }, + } + + assertIdealFeeRate := func(c *LightningChannel, netFee, minRelay, + maxAnchorCommit chainfee.SatPerKWeight, + maxFeeAlloc float64, expectedFeeRate chainfee.SatPerKWeight) { + + feeRate := c.IdealCommitFeeRate( + netFee, minRelay, maxAnchorCommit, maxFeeAlloc, + ) + + // Due to rounding inaccuracies when calculating the fee rate a + // relative error of 0.1% is tolerated. + require.InEpsilonf( + t, float64(expectedFeeRate), float64(feeRate), + 1e-3, "expected max fee rate:%d, got max "+ + "fee rate :%d", currentFeeRate, + maxFeeRate, + ) + + if err := c.validateFeeRate(feeRate); err != nil { + t.Fatalf("fee rate validation failed: %v", err) + } + } + t.Run("non-anchor-channel", func(t *testing.T) { - aliceChannel, _, err := CreateTestChannels( - t, channeldb.SingleFunderTweaklessBit, - ) - if err != nil { - t.Fatalf("unable to create test channels: %v", err) + for _, testCase := range nonAnchorCases { + tc := testCase + + assertIdealFeeRate( + tc.channel, tc.networkFeeRate, tc.minRelayFee, + tc.maxAnchorCommitFee, tc.maxFeeAlloc, + tc.expectedFeeRate, + ) } - - err = quick.Check(propertyTest(aliceChannel), nil) - if err != nil { - t.Fatal(err) - } - - // If the maximum fee is lower than the network fee and above - // the min relay fee, use the maximum fee. - assertIdealFeeRate( - aliceChannel, 700000000, 0, 0, 1.0, 676794154, - ) - - // If the network fee is lower than the max fee and above the - // min relay fee, use the network fee. - assertIdealFeeRate( - aliceChannel, 500000000, 0, 0, 1.0, 500000000, - ) - - // If the fee is below the minimum relay fee, and the min relay - // fee is less than our max fee, use the minimum relay fee. - assertIdealFeeRate( - aliceChannel, 500000000, 600000000, 0, 1.0, 600000000, - ) - - // The absolute maximum fee rate we can pay (ie, using a max - // allocation of 1) is still below the minimum relay fee. In - // this case, use the absolute max fee. - assertIdealFeeRate( - aliceChannel, 800000000, 700000000, 0, 0.001, - 676794154, - ) }) - // Test ideal fee rates for an anchor channel t.Run("anchor-channel", func(t *testing.T) { - anchorChannel, _, err := CreateTestChannels( - t, channeldb.SingleFunderTweaklessBit| - channeldb.AnchorOutputsBit| - channeldb.ZeroHtlcTxFeeBit, - ) - if err != nil { - t.Fatalf("unable to create test channels: %v", err) + for _, testCase := range anchorCases { + tc := testCase + + assertIdealFeeRate( + tc.channel, tc.networkFeeRate, tc.minRelayFee, + tc.maxAnchorCommitFee, tc.maxFeeAlloc, + tc.expectedFeeRate, + ) } - - err = quick.Check(propertyTest(anchorChannel), nil) - if err != nil { - t.Fatal(err) - } - - // Anchor commitments are heavier, hence the same allocation - // leads to slightly lower fee rates. - assertIdealFeeRate( - anchorChannel, 700000000, 0, chainfee.FeePerKwFloor, - 0.1, chainfee.FeePerKwFloor, - ) - - // If the maximum fee is lower than the network fee, use the - // maximum fee. - assertIdealFeeRate( - anchorChannel, 700000000, 0, 1000000, 0.001, 435941, - ) - - assertIdealFeeRate( - anchorChannel, 700000000, 0, 700, 0.000001, 435, - ) - - assertIdealFeeRate( - anchorChannel, 700000000, 0, 1000000, 0.0000001, - chainfee.FeePerKwFloor, - ) - - // If the maximum anchor commit fee rate is less than the - // maximum fee, use the max anchor commit fee since this is an - // anchor channel. - assertIdealFeeRate( - anchorChannel, 700000000, 0, 400000, 0.001, 400000, - ) - - // If the minimum relay fee is above the max anchor commitment - // fee rate but still below our max fee rate then use the min - // relay fee. - assertIdealFeeRate( - anchorChannel, 700000000, 400000, 300000, 0.001, - 400000, - ) - - // If the min relay fee is above the ideal max fee rate but is - // below the max fee rate when the max fee allocation is set to - // 1, the minimum relay fee is used. - assertIdealFeeRate( - anchorChannel, 700000000, 500000, 300000, 0.001, - 500000, - ) - - // The absolute maximum fee rate we can pay (ie, using a max - // allocation of 1) is still below the minimum relay fee. In - // this case, use the absolute max fee. - assertIdealFeeRate( - anchorChannel, 700000000, 450000000, 300000, 0.001, - 435941555, - ) }) }