Merge pull request #7805 from ziggie1984/updatefee-limiter
Limit FeeRate change for the UpdateFee msg to prevent sharp changes
This commit is contained in:
4 files changed
+375
-145
No files matched your search
@@ -56,7 +56,7 @@
|
|||||||
* [Fixed](https://github.com/lightningnetwork/lnd/pull/8096) a case where `lnd`
|
* [Fixed](https://github.com/lightningnetwork/lnd/pull/8096) a case where `lnd`
|
||||||
might dip below its channel reserve when htlcs are added concurrently. A
|
might dip below its channel reserve when htlcs are added concurrently. A
|
||||||
fee buffer (additional balance) is now always kept on the local side ONLY
|
fee buffer (additional balance) is now always kept on the local side ONLY
|
||||||
if the channel was opened locally. This is in accordance with the BOTL 02
|
if the channel was opened locally. This is in accordance with the BOLT 02
|
||||||
specification and protects against sharp fee changes because there is always
|
specification and protects against sharp fee changes because there is always
|
||||||
this buffer which can be used to increase the commitment fee and it also
|
this buffer which can be used to increase the commitment fee and it also
|
||||||
protects against the case where htlcs are added asynchronously resulting in
|
protects against the case where htlcs are added asynchronously resulting in
|
||||||
@@ -90,10 +90,18 @@
|
|||||||
precision issue when querying payments and invoices using the start and end
|
precision issue when querying payments and invoices using the start and end
|
||||||
date filters.
|
date filters.
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
* [Fixed](https://github.com/lightningnetwork/lnd/pull/8496) an issue where
|
* [Fixed](https://github.com/lightningnetwork/lnd/pull/8496) an issue where
|
||||||
`locked_balance` is not updated in `WalletBalanceResponse` when outputs are
|
`locked_balance` is not updated in `WalletBalanceResponse` when outputs are
|
||||||
reserved for `OpenChannel` by using non-volatile leases instead of volatile
|
reserved for `OpenChannel` by using non-volatile leases instead of volatile
|
||||||
locks.
|
locks.
|
||||||
|
=======
|
||||||
|
* [Fixed](https://github.com/lightningnetwork/lnd/pull/7805) a case where `lnd`
|
||||||
|
might propose a low fee rate for the channel (when initiator) due to the
|
||||||
|
mempool not having enough data yet or the channel might be drained locally
|
||||||
|
which with the default fee allocation in place will eventually lead to the
|
||||||
|
downsizing to the fee floor (1 sat/vbyte) in the worst case.
|
||||||
|
>>>>>>> ae6f11c56 (docs: add release-notes.)
|
||||||
|
|
||||||
# New Features
|
# New Features
|
||||||
## Functional Enhancements
|
## Functional Enhancements
|
||||||
@@ -328,7 +336,7 @@
|
|||||||
option.
|
option.
|
||||||
|
|
||||||
* [Update min_final_cltv_expiry_delta](https://github.com/lightningnetwork/lnd/pull/8308).
|
* [Update min_final_cltv_expiry_delta](https://github.com/lightningnetwork/lnd/pull/8308).
|
||||||
This only effects external invoices which do not supply the
|
This only affects external invoices which do not supply the
|
||||||
min_final_cltv_expiry parameter. LND has NOT allowed the creation of invoices
|
min_final_cltv_expiry parameter. LND has NOT allowed the creation of invoices
|
||||||
with a lower min_final_cltv_expiry_delta value than 18 blocks since
|
with a lower min_final_cltv_expiry_delta value than 18 blocks since
|
||||||
LND 0.11.0.
|
LND 0.11.0.
|
||||||
|
|||||||
@@ -311,6 +311,8 @@ type BitcoindEstimator struct {
|
|||||||
// to "CONSERVATIVE".
|
// to "CONSERVATIVE".
|
||||||
feeMode string
|
feeMode string
|
||||||
|
|
||||||
|
// TODO(ziggie): introduce an interface for the client to enhance
|
||||||
|
// testability of the estimator.
|
||||||
bitcoindConn *rpcclient.Client
|
bitcoindConn *rpcclient.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,6 +474,13 @@ func (b *BitcoindEstimator) fetchEstimate(confTarget uint32) (SatPerKWeight, err
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bitcoind will not report any fee estimation if it has not enough
|
||||||
|
// data available hence the fee will remain zero. We return an error
|
||||||
|
// here to make sure that we do not use the min relay fee instead.
|
||||||
|
if satPerKB == 0 {
|
||||||
|
return 0, fmt.Errorf("fee estimation data not available yet")
|
||||||
|
}
|
||||||
|
|
||||||
// Since we use fee rates in sat/kw internally, we'll convert the
|
// Since we use fee rates in sat/kw internally, we'll convert the
|
||||||
// estimated fee rate from its sat/kb representation to sat/kw.
|
// estimated fee rate from its sat/kb representation to sat/kw.
|
||||||
satPerKw := SatPerKVByte(satPerKB).FeePerKWeight()
|
satPerKw := SatPerKVByte(satPerKB).FeePerKWeight()
|
||||||
@@ -479,14 +488,14 @@ func (b *BitcoindEstimator) fetchEstimate(confTarget uint32) (SatPerKWeight, err
|
|||||||
// Finally, we'll enforce our fee floor.
|
// Finally, we'll enforce our fee floor.
|
||||||
minRelayFee := b.minFeeManager.fetchMinFee()
|
minRelayFee := b.minFeeManager.fetchMinFee()
|
||||||
if satPerKw < minRelayFee {
|
if satPerKw < minRelayFee {
|
||||||
log.Debugf("Estimated fee rate of %v sat/kw is too low, "+
|
log.Debugf("Estimated fee rate of %v is too low, "+
|
||||||
"using fee floor of %v sat/kw instead", satPerKw,
|
"using fee floor of %v instead", satPerKw,
|
||||||
minRelayFee)
|
minRelayFee)
|
||||||
|
|
||||||
satPerKw = minRelayFee
|
satPerKw = minRelayFee
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("Returning %v sat/kw for conf target of %v",
|
log.Debugf("Returning %v for conf target of %v",
|
||||||
int64(satPerKw), confTarget)
|
int64(satPerKw), confTarget)
|
||||||
|
|
||||||
return satPerKw, nil
|
return satPerKw, nil
|
||||||
|
|||||||
+26
-11
@@ -7128,6 +7128,7 @@ func newOutgoingHtlcResolution(signer input.Signer,
|
|||||||
localDelayTweak := input.SingleTweakBytes(
|
localDelayTweak := input.SingleTweakBytes(
|
||||||
keyRing.CommitPoint, localChanCfg.DelayBasePoint.PubKey,
|
keyRing.CommitPoint, localChanCfg.DelayBasePoint.PubKey,
|
||||||
)
|
)
|
||||||
|
|
||||||
return &OutgoingHtlcResolution{
|
return &OutgoingHtlcResolution{
|
||||||
Expiry: htlc.RefundTimeout,
|
Expiry: htlc.RefundTimeout,
|
||||||
SignedTimeoutTx: timeoutTx,
|
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
|
// 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
|
// be useful to determine when we should stop proposing fee updates that exceed
|
||||||
// our maximum allocation.
|
// 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
|
// NOTE: This should only be used for channels in which the local commitment is
|
||||||
// the initiator.
|
// the initiator.
|
||||||
func (lc *LightningChannel) MaxFeeRate(maxAllocation float64) chainfee.SatPerKWeight {
|
func (lc *LightningChannel) MaxFeeRate(
|
||||||
|
maxAllocation float64) (chainfee.SatPerKWeight, float64) {
|
||||||
|
|
||||||
lc.RLock()
|
lc.RLock()
|
||||||
defer lc.RUnlock()
|
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.
|
// exactly why it was introduced to react for sharp fee changes.
|
||||||
availableBalance, weight := lc.availableBalance(AdditionalHtlc)
|
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 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.
|
return maxFeeRate, maxFeeAllocation
|
||||||
maxFeeRate := maxFee / (float64(weight) / 1000)
|
|
||||||
return chainfee.SatPerKWeight(
|
|
||||||
math.Max(maxFeeRate, float64(chainfee.FeePerKwFloor)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IdealCommitFeeRate uses the current network fee, the minimum relay fee,
|
// 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
|
// Get the maximum fee rate that we can use given our max fee allocation
|
||||||
// and given the local reserve balance that we must preserve.
|
// and given the local reserve balance that we must preserve.
|
||||||
maxFeeRate := lc.MaxFeeRate(maxFeeAlloc)
|
maxFeeRate, _ := lc.MaxFeeRate(maxFeeAlloc)
|
||||||
|
|
||||||
var commitFeeRate chainfee.SatPerKWeight
|
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
|
// 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
|
// warning to indicate that the max channel fee allocation option was
|
||||||
// ignored.
|
// ignored.
|
||||||
absoluteMaxFee := lc.MaxFeeRate(1)
|
absoluteMaxFee, _ := lc.MaxFeeRate(1)
|
||||||
if minRelayFeeRate <= absoluteMaxFee {
|
if minRelayFeeRate <= absoluteMaxFee {
|
||||||
lc.log.Warn("Ignoring max channel fee allocation to " +
|
lc.log.Warn("Ignoring max channel fee allocation to " +
|
||||||
"ensure that the commitment fee is above the " +
|
"ensure that the commitment fee is above the " +
|
||||||
|
|||||||
+327
-129
@@ -7515,67 +7515,146 @@ func TestForceCloseBorkedState(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestChannelMaxFeeRate ensures we correctly compute a channel initiator's max
|
// TestChannelMaxFeeRate ensures we correctly compute a channel initiator's max
|
||||||
// fee rate based on an allocation and its available balance. It should never
|
// fee rate based on an allocation and its available balance. When a very low
|
||||||
// dip below the established fee floor.
|
// 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) {
|
func TestChannelMaxFeeRate(t *testing.T) {
|
||||||
t.Parallel()
|
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
|
// propertyTest tests that the validateFeeRate function always passes
|
||||||
// for the output returned by MaxFeeRate for any valid random inputs
|
// for the output returned by MaxFeeRate for any valid random inputs
|
||||||
// fed to MaxFeeRate.
|
// fed to MaxFeeRate.
|
||||||
propertyTest := func(c *LightningChannel) func(alloc maxAlloc) bool {
|
propertyTest := func(c *LightningChannel) func(alloc maxAlloc) bool {
|
||||||
return func(ma maxAlloc) bool {
|
return func(ma maxAlloc) bool {
|
||||||
maxFeeRate := c.MaxFeeRate(float64(ma))
|
maxFeeRate, _ := c.MaxFeeRate(float64(ma))
|
||||||
return c.validateFeeRate(maxFeeRate) == nil
|
return c.validateFeeRate(maxFeeRate) == nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
aliceChannel, _, err := CreateTestChannels(
|
// Create a non-anchor and an anchor channel setup.
|
||||||
|
nonAnchorChannel, _, err := CreateTestChannels(
|
||||||
t, channeldb.SingleFunderTweaklessBit,
|
t, channeldb.SingleFunderTweaklessBit,
|
||||||
)
|
)
|
||||||
require.NoError(t, err, "unable to create test channels")
|
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(
|
anchorChannel, _, err := CreateTestChannels(
|
||||||
t, channeldb.SingleFunderTweaklessBit|
|
t, channeldb.SingleFunderTweaklessBit|
|
||||||
channeldb.AnchorOutputsBit|channeldb.ZeroHtlcTxFeeBit,
|
channeldb.AnchorOutputsBit|channeldb.ZeroHtlcTxFeeBit,
|
||||||
)
|
)
|
||||||
require.NoError(t, err, "unable to create test channels")
|
require.NoError(t, err, "unable to create test channels")
|
||||||
|
|
||||||
if err = quick.Check(propertyTest(anchorChannel), nil); err != nil {
|
// Run the property tests for both channel types.
|
||||||
t.Fatal(err)
|
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
|
for _, testCase := range testCases {
|
||||||
// to slightly lower fee rates.
|
tc := testCase
|
||||||
assertMaxFeeRate(anchorChannel, 1.0, 435941555)
|
|
||||||
assertMaxFeeRate(anchorChannel, 0.001, 435941)
|
maxFeeRate, feeAllocation := tc.channel.MaxFeeRate(
|
||||||
assertMaxFeeRate(anchorChannel, 0.000001, 435)
|
tc.maxFeeAlloc,
|
||||||
assertMaxFeeRate(anchorChannel, 0.0000001, chainfee.FeePerKwFloor)
|
)
|
||||||
|
|
||||||
|
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
|
// TestIdealCommitFeeRate tests that we correctly compute the ideal commitment
|
||||||
@@ -7584,23 +7663,6 @@ func TestChannelMaxFeeRate(t *testing.T) {
|
|||||||
func TestIdealCommitFeeRate(t *testing.T) {
|
func TestIdealCommitFeeRate(t *testing.T) {
|
||||||
t.Parallel()
|
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
|
// propertyTest tests that the validateFeeRate function always passes
|
||||||
// for the output returned by IdealCommitFeeRate for any valid random
|
// for the output returned by IdealCommitFeeRate for any valid random
|
||||||
// inputs fed to IdealCommitFeeRate.
|
// inputs fed to IdealCommitFeeRate.
|
||||||
@@ -7621,49 +7683,14 @@ func TestIdealCommitFeeRate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test ideal fee rates for a non-anchor channel
|
// Create a non-anchor channel and an anchor test channel.
|
||||||
t.Run("non-anchor-channel", func(t *testing.T) {
|
nonAnchorChannel, _, err := CreateTestChannels(
|
||||||
aliceChannel, _, err := CreateTestChannels(
|
|
||||||
t, channeldb.SingleFunderTweaklessBit,
|
t, channeldb.SingleFunderTweaklessBit,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to create test channels: %v", err)
|
t.Fatalf("unable to create test channels: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
anchorChannel, _, err := CreateTestChannels(
|
||||||
t, channeldb.SingleFunderTweaklessBit|
|
t, channeldb.SingleFunderTweaklessBit|
|
||||||
channeldb.AnchorOutputsBit|
|
channeldb.AnchorOutputsBit|
|
||||||
@@ -7673,63 +7700,234 @@ func TestIdealCommitFeeRate(t *testing.T) {
|
|||||||
t.Fatalf("unable to create test channels: %v", err)
|
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)
|
err = quick.Check(propertyTest(anchorChannel), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Anchor commitments are heavier, hence the same allocation
|
// maxFeeRate is a helper function which calculates the maximum fee rate
|
||||||
// leads to slightly lower fee rates.
|
// a channel is allowed to allocate to fees. It does not take a minimum
|
||||||
assertIdealFeeRate(
|
// fee rate into account.
|
||||||
anchorChannel, 700000000, 0, chainfee.FeePerKwFloor,
|
maxFeeRate := func(c *LightningChannel,
|
||||||
0.1, chainfee.FeePerKwFloor,
|
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),
|
||||||
)
|
)
|
||||||
|
|
||||||
// If the maximum fee is lower than the network fee, use the
|
return maxFeeRate
|
||||||
// maximum fee.
|
}
|
||||||
assertIdealFeeRate(
|
|
||||||
anchorChannel, 700000000, 0, 1000000, 0.001, 435941,
|
// 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,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertIdealFeeRate(
|
// Due to rounding inaccuracies when calculating the fee rate a
|
||||||
anchorChannel, 700000000, 0, 700, 0.000001, 435,
|
// 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,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertIdealFeeRate(
|
if err := c.validateFeeRate(feeRate); err != nil {
|
||||||
anchorChannel, 700000000, 0, 1000000, 0.0000001,
|
t.Fatalf("fee rate validation failed: %v", err)
|
||||||
chainfee.FeePerKwFloor,
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
// If the maximum anchor commit fee rate is less than the
|
t.Run("non-anchor-channel", func(t *testing.T) {
|
||||||
// maximum fee, use the max anchor commit fee since this is an
|
for _, testCase := range nonAnchorCases {
|
||||||
// anchor channel.
|
tc := testCase
|
||||||
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(
|
assertIdealFeeRate(
|
||||||
anchorChannel, 700000000, 400000, 300000, 0.001,
|
tc.channel, tc.networkFeeRate, tc.minRelayFee,
|
||||||
400000,
|
tc.maxAnchorCommitFee, tc.maxFeeAlloc,
|
||||||
|
tc.expectedFeeRate,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// If the min relay fee is above the ideal max fee rate but is
|
t.Run("anchor-channel", func(t *testing.T) {
|
||||||
// below the max fee rate when the max fee allocation is set to
|
for _, testCase := range anchorCases {
|
||||||
// 1, the minimum relay fee is used.
|
tc := testCase
|
||||||
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(
|
assertIdealFeeRate(
|
||||||
anchorChannel, 700000000, 450000000, 300000, 0.001,
|
tc.channel, tc.networkFeeRate, tc.minRelayFee,
|
||||||
435941555,
|
tc.maxAnchorCommitFee, tc.maxFeeAlloc,
|
||||||
|
tc.expectedFeeRate,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user